258 lines
6.3 KiB
Go
258 lines
6.3 KiB
Go
|
// Package dhcp6 implements a DHCPv6 client.
|
||
|
package dhcp6
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"log"
|
||
|
"net"
|
||
|
"time"
|
||
|
|
||
|
"github.com/insomniacslk/dhcp/dhcpv6"
|
||
|
)
|
||
|
|
||
|
type ClientConfig struct {
|
||
|
InterfaceName string // e.g. eth0
|
||
|
|
||
|
// LocalAddr allows overwriting the source address used for sending DHCPv6
|
||
|
// packets. It defaults to the first link-local address of InterfaceName.
|
||
|
LocalAddr *net.UDPAddr
|
||
|
|
||
|
// RemoteAddr allows addressing a specific DHCPv6 server. It defaults to
|
||
|
// the dhcpv6.AllDHCPRelayAgentsAndServers multicast address.
|
||
|
RemoteAddr *net.UDPAddr
|
||
|
|
||
|
Conn net.PacketConn // for testing
|
||
|
TransactionIDs []uint32 // for testing
|
||
|
}
|
||
|
|
||
|
// Config contains the obtained network configuration.
|
||
|
type Config struct {
|
||
|
RenewAfter time.Time `json:"valid_until"`
|
||
|
Prefixes []net.IPNet `json:"prefixes"` // e.g. 2a02:168:4a00::/48
|
||
|
DNS []string `json:"dns"` // e.g. 2001:1620:2777:1::10, 2001:1620:2777:2::20
|
||
|
}
|
||
|
|
||
|
type Client struct {
|
||
|
interfaceName string
|
||
|
raddr *net.UDPAddr
|
||
|
timeNow func() time.Time
|
||
|
|
||
|
cfg Config
|
||
|
err error
|
||
|
|
||
|
Conn net.PacketConn // TODO: unexport
|
||
|
transactionIDs []uint32
|
||
|
|
||
|
ReadTimeout time.Duration
|
||
|
WriteTimeout time.Duration
|
||
|
|
||
|
RemoteAddr net.Addr
|
||
|
}
|
||
|
|
||
|
func NewClient(cfg ClientConfig) (*Client, error) {
|
||
|
// if no LocalAddr is specified, get the interface's link-local address
|
||
|
laddr := cfg.LocalAddr
|
||
|
if laddr == nil {
|
||
|
llAddr, err := dhcpv6.GetLinkLocalAddr(cfg.InterfaceName)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
laddr = &net.UDPAddr{
|
||
|
IP: *llAddr,
|
||
|
Port: dhcpv6.DefaultClientPort,
|
||
|
Zone: cfg.InterfaceName,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// if no RemoteAddr is specified, use AllDHCPRelayAgentsAndServers
|
||
|
raddr := cfg.RemoteAddr
|
||
|
if raddr == nil {
|
||
|
raddr = &net.UDPAddr{
|
||
|
IP: dhcpv6.AllDHCPRelayAgentsAndServers,
|
||
|
Port: dhcpv6.DefaultServerPort,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// prepare the socket to listen on for replies
|
||
|
conn := cfg.Conn
|
||
|
if conn == nil {
|
||
|
udpConn, err := net.ListenUDP("udp6", laddr)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
conn = udpConn
|
||
|
}
|
||
|
|
||
|
return &Client{
|
||
|
interfaceName: cfg.InterfaceName,
|
||
|
timeNow: time.Now,
|
||
|
raddr: raddr,
|
||
|
Conn: conn,
|
||
|
transactionIDs: cfg.TransactionIDs,
|
||
|
ReadTimeout: dhcpv6.DefaultReadTimeout,
|
||
|
WriteTimeout: dhcpv6.DefaultWriteTimeout,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
func (c *Client) Close() error {
|
||
|
return c.Conn.Close()
|
||
|
}
|
||
|
|
||
|
const maxUDPReceivedPacketSize = 8192 // arbitrary size. Theoretically could be up to 65kb
|
||
|
|
||
|
func (c *Client) sendReceive(packet dhcpv6.DHCPv6, expectedType dhcpv6.MessageType) (dhcpv6.DHCPv6, error) {
|
||
|
if packet == nil {
|
||
|
return nil, fmt.Errorf("Packet to send cannot be nil")
|
||
|
}
|
||
|
if expectedType == dhcpv6.MSGTYPE_NONE {
|
||
|
// infer the expected type from the packet being sent
|
||
|
if packet.Type() == dhcpv6.SOLICIT {
|
||
|
expectedType = dhcpv6.ADVERTISE
|
||
|
} else if packet.Type() == dhcpv6.REQUEST {
|
||
|
expectedType = dhcpv6.REPLY
|
||
|
} else if packet.Type() == dhcpv6.RELAY_FORW {
|
||
|
expectedType = dhcpv6.RELAY_REPL
|
||
|
} else if packet.Type() == dhcpv6.LEASEQUERY {
|
||
|
expectedType = dhcpv6.LEASEQUERY_REPLY
|
||
|
} // and probably more
|
||
|
}
|
||
|
|
||
|
// send the packet out
|
||
|
c.Conn.SetWriteDeadline(time.Now().Add(c.WriteTimeout))
|
||
|
if _, err := c.Conn.WriteTo(packet.ToBytes(), c.raddr); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// wait for a reply
|
||
|
c.Conn.SetReadDeadline(time.Now().Add(c.ReadTimeout))
|
||
|
var (
|
||
|
adv dhcpv6.DHCPv6
|
||
|
isMessage bool
|
||
|
)
|
||
|
msg, ok := packet.(*dhcpv6.DHCPv6Message)
|
||
|
if ok {
|
||
|
isMessage = true
|
||
|
}
|
||
|
for {
|
||
|
buf := make([]byte, maxUDPReceivedPacketSize)
|
||
|
n, _, err := c.Conn.ReadFrom(buf)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
adv, err = dhcpv6.FromBytes(buf[:n])
|
||
|
if err != nil {
|
||
|
log.Printf("non-DHCP: %v", err)
|
||
|
// skip non-DHCP packets
|
||
|
continue
|
||
|
}
|
||
|
if recvMsg, ok := adv.(*dhcpv6.DHCPv6Message); ok && isMessage {
|
||
|
// if a regular message, check the transaction ID first
|
||
|
// XXX should this unpack relay messages and check the XID of the
|
||
|
// inner packet too?
|
||
|
if msg.TransactionID() != recvMsg.TransactionID() {
|
||
|
log.Printf("different XID")
|
||
|
// different XID, we don't want this packet for sure
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
if expectedType == dhcpv6.MSGTYPE_NONE {
|
||
|
// just take whatever arrived
|
||
|
break
|
||
|
} else if adv.Type() == expectedType {
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
return adv, nil
|
||
|
}
|
||
|
|
||
|
func (c *Client) solicit(solicit dhcpv6.DHCPv6) (dhcpv6.DHCPv6, dhcpv6.DHCPv6, error) {
|
||
|
var err error
|
||
|
if solicit == nil {
|
||
|
solicit, err = dhcpv6.NewSolicitForInterface(c.interfaceName)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
}
|
||
|
if len(c.transactionIDs) > 0 {
|
||
|
id := c.transactionIDs[0]
|
||
|
c.transactionIDs = c.transactionIDs[1:]
|
||
|
solicit.(*dhcpv6.DHCPv6Message).SetTransactionID(id)
|
||
|
}
|
||
|
advertise, err := c.sendReceive(solicit, dhcpv6.MSGTYPE_NONE)
|
||
|
return solicit, advertise, err
|
||
|
}
|
||
|
|
||
|
func (c *Client) request(advertise, request dhcpv6.DHCPv6) (dhcpv6.DHCPv6, dhcpv6.DHCPv6, error) {
|
||
|
if request == nil {
|
||
|
var err error
|
||
|
request, err = dhcpv6.NewRequestFromAdvertise(advertise)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
}
|
||
|
if len(c.transactionIDs) > 0 {
|
||
|
id := c.transactionIDs[0]
|
||
|
c.transactionIDs = c.transactionIDs[1:]
|
||
|
request.(*dhcpv6.DHCPv6Message).SetTransactionID(id)
|
||
|
}
|
||
|
reply, err := c.sendReceive(request, dhcpv6.MSGTYPE_NONE)
|
||
|
return request, reply, err
|
||
|
}
|
||
|
|
||
|
func (c *Client) ObtainOrRenew() bool {
|
||
|
_, advertise, err := c.solicit(nil)
|
||
|
if err != nil {
|
||
|
c.err = err
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
_, reply, err := c.request(advertise, nil)
|
||
|
if err != nil {
|
||
|
c.err = err
|
||
|
return true
|
||
|
}
|
||
|
var newCfg Config
|
||
|
for _, opt := range reply.Options() {
|
||
|
switch o := opt.(type) {
|
||
|
case *dhcpv6.OptIAForPrefixDelegation:
|
||
|
t1 := c.timeNow().Add(time.Duration(o.T1()) * time.Second)
|
||
|
if t1.Before(newCfg.RenewAfter) || newCfg.RenewAfter.IsZero() {
|
||
|
newCfg.RenewAfter = t1
|
||
|
}
|
||
|
for b := o.Options(); len(b) > 0; {
|
||
|
sopt, err := dhcpv6.ParseOption(b)
|
||
|
if err != nil {
|
||
|
c.err = err
|
||
|
return true
|
||
|
}
|
||
|
b = b[4+sopt.Length():]
|
||
|
|
||
|
prefix, ok := sopt.(*dhcpv6.OptIAPrefix)
|
||
|
if !ok {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
newCfg.Prefixes = append(newCfg.Prefixes, net.IPNet{
|
||
|
IP: prefix.IPv6Prefix(),
|
||
|
Mask: net.CIDRMask(int(prefix.PrefixLength()), 128),
|
||
|
})
|
||
|
}
|
||
|
|
||
|
case *dhcpv6.OptDNSRecursiveNameServer:
|
||
|
for _, ns := range o.NameServers {
|
||
|
newCfg.DNS = append(newCfg.DNS, ns.String())
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
c.cfg = newCfg
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
func (c *Client) Err() error {
|
||
|
return c.err
|
||
|
}
|
||
|
|
||
|
func (c *Client) Config() Config {
|
||
|
return c.cfg
|
||
|
}
|