2018-06-15 09:58:51 +02:00

217 lines
5.4 KiB
Go

// Package dhcp4 implements a DHCPv4 client.
package dhcp4
import (
"bytes"
"encoding/binary"
"fmt"
"log"
"net"
"sync"
"syscall"
"time"
"golang.org/x/sys/unix"
"github.com/d2g/dhcp4"
"github.com/d2g/dhcp4client"
)
type Config struct {
RenewAfter time.Time `json:"valid_until"`
ClientIP string `json:"client_ip"` // e.g. 85.195.207.62
SubnetMask string `json:"subnet_mask"` // e.g. 255.255.255.128
Router string `json:"router"` // e.g. 85.195.207.1
DNS []string `json:"dns"` // e.g. 77.109.128.2, 213.144.129.20
}
type Client struct {
Interface *net.Interface // e.g. net.InterfaceByName("eth0")
err error
once sync.Once
dhcp *dhcp4client.Client
connection dhcp4client.ConnectionInt
hardwareAddr net.HardwareAddr
cfg Config
timeNow func() time.Time
generateXID func([]byte)
// last DHCPACK packet for renewal/release
ack dhcp4.Packet
}
// ObtainOrRenew returns false when encountering a permanent error.
func (c *Client) ObtainOrRenew() bool {
c.once.Do(func() {
if c.timeNow == nil {
c.timeNow = time.Now
}
if c.connection == nil && c.Interface != nil {
pktsock, err := dhcp4client.NewPacketSock(c.Interface.Index)
if err != nil {
c.err = err
return
}
c.connection = pktsock
}
if c.connection == nil && c.Interface == nil {
c.err = fmt.Errorf("Interface is nil")
return
}
if c.hardwareAddr == nil {
c.hardwareAddr = c.Interface.HardwareAddr
}
dhcp, err := dhcp4client.New(
dhcp4client.HardwareAddr(c.hardwareAddr),
dhcp4client.Timeout(5*time.Second),
dhcp4client.Broadcast(false),
dhcp4client.Connection(c.connection),
dhcp4client.GenerateXID(c.generateXID),
)
if err != nil {
c.err = err
return
}
c.dhcp = dhcp
})
c.err = nil // clear previous error
// TODO: renew if c.ack != nil, fall back if renewal fails
ok, ack, err := c.dhcpRequest()
if err != nil {
if errno, ok := err.(syscall.Errno); ok && errno == syscall.EAGAIN {
c.err = fmt.Errorf("DHCP: timeout (server(s) unreachable)")
return true // temporary error
}
c.err = fmt.Errorf("DHCP: %v", err)
return true // temporary error
}
if !ok {
c.err = fmt.Errorf("received DHCPNAK")
return true // temporary error
}
c.ack = ack
opts := ack.ParseOptions()
// DHCPACK (described in RFC2131 4.3.1)
// - yiaddr: IP address assigned to client
c.cfg.ClientIP = ack.YIAddr().String()
if b, ok := opts[dhcp4.OptionSubnetMask]; ok {
mask := net.IPMask(b)
c.cfg.SubnetMask = fmt.Sprintf("%d.%d.%d.%d", mask[0], mask[1], mask[2], mask[3])
}
// if b, ok := opts[dhcp4.OptionBroadcastAddress]; ok {
// if err := cs.SetBroadcast(net.IP(b)); err != nil {
// log.Fatalf("setBroadcast(%v): %v", net.IP(b), err)
// }
// }
if b, ok := opts[dhcp4.OptionRouter]; ok {
c.cfg.Router = net.IP(b).String()
}
if b, ok := opts[dhcp4.OptionDomainNameServer]; ok {
c.cfg.DNS = nil
for len(b) > 0 {
c.cfg.DNS = append(c.cfg.DNS, net.IP(b[:4]).String())
b = b[4:]
}
}
leaseTime := 10 * time.Minute // seems sensible as a fallback
if b, ok := opts[dhcp4.OptionIPAddressLeaseTime]; ok && len(b) == 4 {
leaseTime = parseDHCPDuration(b)
}
// As per RFC 2131 section 4.4.5:
// renewal time defaults to 50% of the lease time
renewalTime := time.Duration(float64(leaseTime) * 0.5)
if b, ok := opts[dhcp4.OptionRenewalTimeValue]; ok && len(b) == 4 {
renewalTime = parseDHCPDuration(b)
}
c.cfg.RenewAfter = c.timeNow().Add(renewalTime)
return true
}
func (c *Client) Release() error {
err := c.dhcp.Release(c.ack)
c.ack = nil
return err
}
func (c *Client) Err() error {
return c.err
}
func (c *Client) Config() Config {
return c.cfg
}
func parseDHCPDuration(b []byte) time.Duration {
return time.Duration(binary.BigEndian.Uint32(b)) * time.Second
}
func (c *Client) addHostname(p *dhcp4.Packet) {
var utsname unix.Utsname
if err := unix.Uname(&utsname); err != nil {
log.Fatal(err)
}
nnb := utsname.Nodename[:bytes.IndexByte(utsname.Nodename[:], 0)]
p.AddOption(dhcp4.OptionHostName, nnb)
}
func (c *Client) addClientId(p *dhcp4.Packet) {
id := make([]byte, len(c.hardwareAddr)+1)
id[0] = 1 // hardware type ethernet, https://tools.ietf.org/html/rfc1700
copy(id[1:], c.hardwareAddr)
p.AddOption(dhcp4.OptionClientIdentifier, id)
}
// dhcpRequest is a copy of (dhcp4client/Client).Request which
// includes the hostname.
func (c *Client) dhcpRequest() (bool, dhcp4.Packet, error) {
var last dhcp4.Packet
if c.ack == nil {
discoveryPacket := c.dhcp.DiscoverPacket()
c.addHostname(&discoveryPacket)
c.addClientId(&discoveryPacket)
discoveryPacket.PadToMinSize()
if err := c.dhcp.SendPacket(discoveryPacket); err != nil {
return false, discoveryPacket, err
}
offerPacket, err := c.dhcp.GetOffer(&discoveryPacket)
if err != nil {
return false, offerPacket, err
}
last = offerPacket
} else {
last = c.ack
}
requestPacket := c.dhcp.RequestPacket(&last)
c.addHostname(&requestPacket)
c.addClientId(&requestPacket)
requestPacket.PadToMinSize()
if err := c.dhcp.SendPacket(requestPacket); err != nil {
return false, requestPacket, err
}
acknowledgement, err := c.dhcp.GetAcknowledgement(&requestPacket)
if err != nil {
return false, acknowledgement, err
}
acknowledgementOptions := acknowledgement.ParseOptions()
if dhcp4.MessageType(acknowledgementOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.ACK {
c.ack = nil // start over
return false, acknowledgement, nil
}
return true, acknowledgement, nil
}