From 8c55c5ba444cb079c254604787da78dd6e0d26d2 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Wed, 21 Nov 2018 08:18:58 +0100 Subject: [PATCH] dhcp4: switch to github.com/rtr7/dhcp4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- cmd/dhcp4/dhcp4.go | 33 +- internal/dhcp4/dhcp4.go | 282 ++++++++++-------- internal/dhcp4/dhcp4_test.go | 7 +- internal/testing/pcapreplayer/pcapreplayer.go | 39 ++- 4 files changed, 208 insertions(+), 153 deletions(-) diff --git a/cmd/dhcp4/dhcp4.go b/cmd/dhcp4/dhcp4.go index e6010da..de7b277 100644 --- a/cmd/dhcp4/dhcp4.go +++ b/cmd/dhcp4/dhcp4.go @@ -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 { diff --git a/internal/dhcp4/dhcp4.go b/internal/dhcp4/dhcp4.go index 6e3660c..95cd5dd 100644 --- a/internal/dhcp4/dhcp4.go +++ b/internal/dhcp4/dhcp4.go @@ -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 } diff --git a/internal/dhcp4/dhcp4_test.go b/internal/dhcp4/dhcp4_test.go index 0a6d1ee..1a05f83 100644 --- a/internal/dhcp4/dhcp4_test.go +++ b/internal/dhcp4/dhcp4_test.go @@ -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 }, } diff --git a/internal/testing/pcapreplayer/pcapreplayer.go b/internal/testing/pcapreplayer/pcapreplayer.go index b2e862f..5ac211b 100644 --- a/internal/testing/pcapreplayer/pcapreplayer.go +++ b/internal/testing/pcapreplayer/pcapreplayer.go @@ -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 }