dhcp4: switch to github.com/rtr7/dhcp4

All existing DHCPv4 packages I looked at were unappealing for one reason or
another, so we’re now using a little helper to glue github.com/google/gopacket
and github.com/mdlayher/raw together, which suffices for our use-case and gives
us more control.
This commit is contained in:
Michael Stapelberg 2018-11-21 08:18:58 +01:00
parent 30e9a6677b
commit 8c55c5ba44
4 changed files with 208 additions and 153 deletions

View File

@ -28,6 +28,9 @@ import (
"syscall"
"time"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/jpillora/backoff"
"github.com/rtr7/router7/internal/dhcp4"
"github.com/rtr7/router7/internal/notify"
"github.com/rtr7/router7/internal/teelogger"
@ -45,9 +48,15 @@ func logic() error {
return err
}
const ackFn = "/perm/dhcp4/wire/ack"
ack, err := ioutil.ReadFile(ackFn)
var ack *layers.DHCPv4
ackB, err := ioutil.ReadFile(ackFn)
if err != nil && !os.IsNotExist(err) {
log.Printf("Loading previous DHCPACK packet from %s: %v", ackFn, err)
} else {
pkt := gopacket.NewPacket(ackB, layers.LayerTypeDHCPv4, gopacket.DecodeOptions{})
if dhcp, ok := pkt.Layer(layers.LayerTypeDHCPv4).(*layers.DHCPv4); ok {
ack = dhcp
}
}
c := dhcp4.Client{
Interface: iface,
@ -55,12 +64,20 @@ func logic() error {
}
usr2 := make(chan os.Signal, 1)
signal.Notify(usr2, syscall.SIGUSR2)
backoff := backoff.Backoff{
Factor: 2,
Jitter: true,
Min: 10 * time.Second,
Max: 1 * time.Minute,
}
for c.ObtainOrRenew() {
if err := c.Err(); err != nil {
log.Printf("Temporary error: %v", err)
time.Sleep(1 * time.Second)
dur := backoff.Duration()
log.Printf("Temporary error: %v (waiting %v)", err, dur)
time.Sleep(dur)
continue
}
backoff.Reset()
log.Printf("lease: %+v", c.Config())
b, err := json.Marshal(c.Config())
if err != nil {
@ -69,7 +86,15 @@ func logic() error {
if err := ioutil.WriteFile(leasePath, b, 0644); err != nil {
return fmt.Errorf("persisting lease to %s: %v", leasePath, err)
}
if err := ioutil.WriteFile(ackFn, c.Ack, 0644); err != nil {
buf := gopacket.NewSerializeBuffer()
gopacket.SerializeLayers(buf,
gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: true,
},
c.Ack,
)
if err := ioutil.WriteFile(ackFn, buf.Bytes(), 0644); err != nil {
return fmt.Errorf("persisting DHCPACK to %s: %v", ackFn, err)
}
if err := notify.Process("/user/netconfigd", syscall.SIGUSR1); err != nil {

View File

@ -17,7 +17,6 @@ package dhcp4
import (
"bytes"
"encoding/binary"
"fmt"
"log"
"net"
@ -25,10 +24,10 @@ import (
"syscall"
"time"
"github.com/google/gopacket/layers"
"github.com/mdlayher/raw"
"github.com/rtr7/dhcp4"
"golang.org/x/sys/unix"
"github.com/d2g/dhcp4"
"github.com/d2g/dhcp4client"
)
type Config struct {
@ -44,15 +43,40 @@ type Client struct {
err error
once sync.Once
dhcp *dhcp4client.Client
connection dhcp4client.ConnectionInt
connection net.PacketConn
hardwareAddr net.HardwareAddr
hostname string
cfg Config
timeNow func() time.Time
generateXID func([]byte)
generateXID func() uint32
// last DHCPACK packet for renewal/release
Ack dhcp4.Packet
Ack *layers.DHCPv4
}
func serverID(pkt *layers.DHCPv4) []layers.DHCPOption {
for _, o := range pkt.Options {
if o.Type == layers.DHCPOptServerID {
return []layers.DHCPOption{o}
}
}
return nil
}
func (c *Client) packet(xid uint32, opts []layers.DHCPOption) *layers.DHCPv4 {
return &layers.DHCPv4{
Operation: layers.DHCPOpRequest,
HardwareType: layers.LinkTypeEthernet,
HardwareLen: uint8(len(layers.EthernetBroadcast)),
HardwareOpts: 0, // TODO: document
Xid: xid,
Secs: 0, // TODO: fill in?
Flags: 0, // TODO: document
ClientHWAddr: c.hardwareAddr,
ServerName: nil,
File: nil,
Options: opts,
}
}
// ObtainOrRenew returns false when encountering a permanent error.
@ -62,12 +86,14 @@ func (c *Client) ObtainOrRenew() bool {
c.timeNow = time.Now
}
if c.connection == nil && c.Interface != nil {
pktsock, err := dhcp4client.NewPacketSock(c.Interface.Index)
conn, err := raw.ListenPacket(c.Interface, syscall.ETH_P_IP, &raw.Config{
LinuxSockDGRAM: true,
})
if err != nil {
c.err = err
return
}
c.connection = pktsock
c.connection = conn
}
if c.connection == nil && c.Interface == nil {
c.err = fmt.Errorf("Interface is nil")
@ -76,21 +102,20 @@ func (c *Client) ObtainOrRenew() bool {
if c.hardwareAddr == nil {
c.hardwareAddr = c.Interface.HardwareAddr
}
dhcp, err := dhcp4client.New(
dhcp4client.HardwareAddr(c.hardwareAddr),
dhcp4client.Timeout(10*time.Second),
dhcp4client.Broadcast(false),
dhcp4client.Connection(c.connection),
dhcp4client.GenerateXID(c.generateXID),
)
if err != nil {
c.err = err
return
if c.generateXID == nil {
c.generateXID = dhcp4.XIDGenerator(c.hardwareAddr)
}
if c.hostname == "" {
var utsname unix.Utsname
if err := unix.Uname(&utsname); err != nil {
log.Fatal(err)
}
c.hostname = string(utsname.Nodename[:bytes.IndexByte(utsname.Nodename[:], 0)])
}
c.dhcp = dhcp
})
// TODO: handle c.err from c.once
c.err = nil // clear previous error
ok, ack, err := c.dhcpRequest()
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)")
@ -99,59 +124,53 @@ func (c *Client) ObtainOrRenew() bool {
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:]
}
}
c.cfg.ClientIP = ack.YourClientIP.String()
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)
var renewalTime *time.Duration
for _, opt := range dhcp4.ParseOptions(c.Ack.Options) {
switch o := opt.(type) {
case *dhcp4.OptSubnetMask:
c.cfg.SubnetMask = fmt.Sprintf("%d.%d.%d.%d", o.Mask[0], o.Mask[1], o.Mask[2], o.Mask[3])
case *dhcp4.OptRouter:
c.cfg.Router = o.Router.String()
case *dhcp4.OptDNS:
c.cfg.DNS = make([]string, len(o.DNS))
for idx, ip := range o.DNS {
c.cfg.DNS[idx] = ip.String()
}
case *dhcp4.OptLeaseTime:
leaseTime = o.LeaseTime
case *dhcp4.OptT1:
renewalTime = &o.T1
}
}
c.cfg.RenewAfter = c.timeNow().Add(renewalTime)
if renewalTime == nil {
d := time.Duration(float64(leaseTime) * 0.5)
renewalTime = &d
}
c.cfg.RenewAfter = c.timeNow().Add(*renewalTime)
return true
}
func (c *Client) Release() error {
err := c.dhcp.Release(c.Ack)
release := c.packet(c.generateXID(), append([]layers.DHCPOption{
dhcp4.MessageTypeOpt(layers.DHCPMsgTypeRelease),
}, serverID(c.Ack)...))
release.ClientIP = c.Ack.YourClientIP
if err := dhcp4.Write(c.connection, release); err != nil {
return err
}
c.Ack = nil
return err
return nil
}
func (c *Client) Err() error {
@ -162,78 +181,77 @@ 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) dhcpRequest() (*layers.DHCPv4, error) {
var last *layers.DHCPv4
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)
}
func (c *Client) addOptionParameterRequest(p *dhcp4.Packet, opts []dhcp4.OptionCode) {
op := make([]byte, len(opts))
for i, o := range opts {
op[i] = byte(o)
}
p.AddOption(dhcp4.OptionParameterRequestList, op)
}
// 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)
c.addOptionParameterRequest(&discoveryPacket, []dhcp4.OptionCode{dhcp4.OptionDomainNameServer, dhcp4.OptionRouter, dhcp4.OptionSubnetMask})
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 {
if c.Ack != nil {
last = c.Ack
} else {
discover := c.packet(c.generateXID(), []layers.DHCPOption{
dhcp4.MessageTypeOpt(layers.DHCPMsgTypeDiscover),
dhcp4.HostnameOpt(c.hostname),
dhcp4.ClientIDOpt(layers.LinkTypeEthernet, c.hardwareAddr),
dhcp4.ParamsRequestOpt(
layers.DHCPOptDNS,
layers.DHCPOptRouter,
layers.DHCPOptSubnetMask),
})
if err := dhcp4.Write(c.connection, discover); err != nil {
return nil, err
}
// Look for DHCPOFFER packet (TODO: RFC)
c.connection.SetDeadline(time.Now().Add(10 * time.Second))
for {
offer, err := dhcp4.Read(c.connection)
if err != nil {
return nil, err
}
if offer == nil {
continue // not a DHCPv4 packet
}
if offer.Xid != discover.Xid {
continue // broadcast reply for different DHCP transaction
}
if !dhcp4.HasMessageType(offer.Options, layers.DHCPMsgTypeOffer) {
continue
}
last = offer
break
}
}
requestPacket := c.dhcp.RequestPacket(&last)
c.addHostname(&requestPacket)
c.addClientId(&requestPacket)
c.addOptionParameterRequest(&requestPacket, []dhcp4.OptionCode{dhcp4.OptionDomainNameServer, dhcp4.OptionRouter, dhcp4.OptionSubnetMask})
requestPacket.PadToMinSize()
if err := c.dhcp.SendPacket(requestPacket); err != nil {
return false, requestPacket, err
// Build a DHCPREQUEST packet:
request := c.packet(last.Xid, append([]layers.DHCPOption{
dhcp4.MessageTypeOpt(layers.DHCPMsgTypeRequest),
dhcp4.RequestIPOpt(last.YourClientIP),
dhcp4.HostnameOpt(c.hostname),
dhcp4.ClientIDOpt(layers.LinkTypeEthernet, c.hardwareAddr),
dhcp4.ParamsRequestOpt(
layers.DHCPOptDNS,
layers.DHCPOptRouter,
layers.DHCPOptSubnetMask),
}, serverID(last)...))
if err := dhcp4.Write(c.connection, request); err != nil {
return nil, err
}
acknowledgement, err := c.dhcp.GetAcknowledgement(&requestPacket)
if err != nil {
return false, acknowledgement, err
c.connection.SetDeadline(time.Now().Add(10 * time.Second))
for {
// Look for DHCPACK packet (described in RFC2131 4.3.1):
ack, err := dhcp4.Read(c.connection)
if err != nil {
return nil, err
}
if ack == nil {
continue // not a DHCPv4 packet
}
if ack.Xid != request.Xid {
continue // broadcast reply for different DHCP transaction
}
if !dhcp4.HasMessageType(ack.Options, layers.DHCPMsgTypeAck) {
continue
}
return ack, nil
}
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
}

View File

@ -46,12 +46,9 @@ func TestDHCP4(t *testing.T) {
hardwareAddr: mac,
timeNow: func() time.Time { return now },
connection: conn,
generateXID: func(b []byte) {
if got, want := len(b), 4; got != want {
t.Fatalf("github.com/d2g/dhcp4client request unexpected amount of bytes: got %d, want %d", got, want)
}
generateXID: func() uint32 {
// TODO: read the transaction ID from the pcap file
copy(b, []byte{0x77, 0x08, 0xd7, 0x24})
return 0x7708d724
},
}

View File

@ -23,7 +23,6 @@ import (
"path/filepath"
"time"
"github.com/d2g/dhcp4client"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcapgo"
@ -182,19 +181,23 @@ type dhcp4conn struct {
// pcap file input, writing packets to pcap file output (if non-empty).
//
// See https://en.wikipedia.org/wiki/Pcap for details on pcap.
func NewDHCP4Conn(input, output string) (dhcp4client.ConnectionInt, error) {
func NewDHCP4Conn(input, output string) (net.PacketConn, error) {
pcapr, pcapw, err := pcapopen(input, output)
return &dhcp4conn{pcapr, pcapw}, err
return &dhcp4conn{pcapr: pcapr, pcapw: pcapw}, err
}
func (r *dhcp4conn) Close() error { return nil }
func (r *dhcp4conn) SetReadTimeout(t time.Duration) error { return nil }
func (r *dhcp4conn) LocalAddr() net.Addr { return nil }
func (r *dhcp4conn) Close() error { return nil }
func (r *dhcp4conn) SetDeadline(t time.Time) error { return nil }
func (r *dhcp4conn) SetReadDeadline(t time.Time) error { return nil }
func (r *dhcp4conn) SetWriteDeadline(t time.Time) error { return nil }
func (r *dhcp4conn) Write(b []byte) error {
func (r *dhcp4conn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
if r.pcapw == nil {
return nil
return len(b), nil
}
return pcapwrite(r.pcapw,
return len(b), pcapwrite(r.pcapw,
&layers.IPv4{
Version: 4,
TTL: 255,
@ -209,8 +212,20 @@ func (r *dhcp4conn) Write(b []byte) error {
b)
}
func (r *dhcp4conn) ReadFrom() ([]byte, net.IP, error) {
buf := make([]byte, 9000)
_, ip, err := readFrom(r.pcapr, buf)
return buf, ip, err
func (r *dhcp4conn) ReadFrom(buf []byte) (int, net.Addr, error) {
data, _, err := r.pcapr.ReadPacketData()
if err != nil {
return 0, nil, err
}
pkt := gopacket.NewPacket(data, layers.LayerTypeEthernet, gopacket.DecodeOptions{})
// TODO: get source IP
eth := pkt.Layer(layers.LayerTypeEthernet)
if eth == nil {
return 0, nil, fmt.Errorf("pcap contained unexpected non-IPv4 packet")
}
//log.Printf("ReadFrom(): %x, %v, pkt = %+v", udp.LayerPayload(), err, pkt)
copy(buf, eth.LayerPayload())
ip := net.ParseIP("192.168.23.1")
return len(eth.LayerPayload()), &net.IPAddr{IP: ip}, nil
}