diff --git a/cmd/dhcp4d/dhcp4d.go b/cmd/dhcp4d/dhcp4d.go index a559e79..c24ea9e 100644 --- a/cmd/dhcp4d/dhcp4d.go +++ b/cmd/dhcp4d/dhcp4d.go @@ -39,7 +39,7 @@ func logic() error { return err } errs := make(chan error) - handler, err := dhcp4d.NewHandler("/perm") + handler, err := dhcp4d.NewHandler("/perm", nil, nil) if err != nil { return err } diff --git a/internal/dhcp4d/dhcp4d.go b/internal/dhcp4d/dhcp4d.go index 598f9b6..7f185cc 100644 --- a/internal/dhcp4d/dhcp4d.go +++ b/internal/dhcp4d/dhcp4d.go @@ -6,11 +6,15 @@ import ( "log" "math/rand" "net" + "syscall" "time" "router7/internal/netconfig" + "github.com/google/gopacket" + "github.com/google/gopacket/layers" "github.com/krolaw/dhcp4" + "github.com/mdlayher/raw" ) type Lease struct { @@ -33,6 +37,8 @@ type Handler struct { options dhcp4.Options leasesHW map[string]*Lease leasesIP map[int]*Lease + rawConn net.PacketConn + iface *net.Interface timeNow func() time.Time @@ -40,16 +46,30 @@ type Handler struct { Leases func([]*Lease) } -func NewHandler(dir string) (*Handler, error) { +func NewHandler(dir string, iface *net.Interface, conn net.PacketConn) (*Handler, error) { serverIP, err := netconfig.LinkAddress(dir, "lan0") if err != nil { return nil, err } + if iface == nil { + iface, err = net.InterfaceByName("lan0") + if err != nil { + return nil, err + } + } + if conn == nil { + conn, err = raw.ListenPacket(iface, syscall.ETH_P_ALL, nil) + if err != nil { + return nil, err + } + } serverIP = serverIP.To4() start := make(net.IP, len(serverIP)) copy(start, serverIP) start[len(start)-1] += 1 return &Handler{ + rawConn: conn, + iface: iface, leasesHW: make(map[string]*Lease), leasesIP: make(map[int]*Lease), serverIP: serverIP, @@ -122,8 +142,47 @@ func (h *Handler) canLease(reqIP net.IP, hwaddr string) int { return -1 // lease unavailable } -// TODO: is ServeDHCP always run from the same goroutine, or do we need locking? func (h *Handler) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dhcp4.Options) dhcp4.Packet { + reply := h.serveDHCP(p, msgType, options) + buf := gopacket.NewSerializeBuffer() + opts := gopacket.SerializeOptions{ + ComputeChecksums: true, + FixLengths: true, + } + ethernet := &layers.Ethernet{ + DstMAC: p.CHAddr(), + SrcMAC: h.iface.HardwareAddr, + EthernetType: layers.EthernetTypeIPv4, + } + + ip := &layers.IPv4{ + Version: 4, + TTL: 255, + SrcIP: h.serverIP, + DstIP: reply.YIAddr(), + Protocol: layers.IPProtocolUDP, + Flags: layers.IPv4DontFragment, + } + udp := &layers.UDP{ + SrcPort: 67, + DstPort: 68, + } + udp.SetNetworkLayerForChecksum(ip) + gopacket.SerializeLayers(buf, opts, + ethernet, + ip, + udp, + gopacket.Payload(reply)) + + if _, err := h.rawConn.WriteTo(buf.Bytes(), &raw.Addr{p.CHAddr()}); err != nil { + log.Printf("WriteTo: %v", err) + } + + return nil +} + +// TODO: is ServeDHCP always run from the same goroutine, or do we need locking? +func (h *Handler) serveDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dhcp4.Options) dhcp4.Packet { reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress]) if reqIP == nil { reqIP = net.IP(p.CIAddr()) @@ -153,14 +212,12 @@ func (h *Handler) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options d return nil // no free leases } - pkt := dhcp4.ReplyPacket(p, + return dhcp4.ReplyPacket(p, dhcp4.Offer, h.serverIP, dhcp4.IPAdd(h.start, free), h.leasePeriod, h.options.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])) - pkt.SetBroadcast(true) - return pkt case dhcp4.Request: if server, ok := options[dhcp4.OptionServerIdentifier]; ok && !net.IP(server).Equal(h.serverIP) { @@ -193,6 +250,7 @@ func (h *Handler) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options d h.leasesIP[leaseNum] = lease h.leasesHW[lease.HardwareAddr] = lease + log.Printf("handed out lease %+v (ip bytes: %#v)", lease, lease.Addr) if h.Leases != nil { var leases []*Lease for _, l := range h.leasesIP { @@ -202,7 +260,6 @@ func (h *Handler) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options d } return dhcp4.ReplyPacket(p, dhcp4.ACK, h.serverIP, reqIP, h.leasePeriod, h.options.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])) - } return nil } diff --git a/internal/dhcp4d/dhcp4d_test.go b/internal/dhcp4d/dhcp4d_test.go index a619e4e..dc1c254 100644 --- a/internal/dhcp4d/dhcp4d_test.go +++ b/internal/dhcp4d/dhcp4d_test.go @@ -47,6 +47,16 @@ const goldenInterfaces = ` } ` +type noopSink struct{} + +func (*noopSink) LocalAddr() net.Addr { return nil } +func (*noopSink) Close() error { return nil } +func (*noopSink) WriteTo(b []byte, addr net.Addr) (n int, err error) { return len(b), nil } +func (*noopSink) SetDeadline(t time.Time) error { return nil } +func (*noopSink) SetReadDeadline(t time.Time) error { return nil } +func (*noopSink) SetWriteDeadline(t time.Time) error { return nil } +func (*noopSink) ReadFrom(buf []byte) (int, net.Addr, error) { return 0, nil, nil } + func testHandler(t *testing.T) (_ *Handler, cleanup func()) { tmpdir, err := ioutil.TempDir("", "dhcp4dtest") if err != nil { @@ -55,7 +65,13 @@ func testHandler(t *testing.T) (_ *Handler, cleanup func()) { if err := ioutil.WriteFile(filepath.Join(tmpdir, "interfaces.json"), []byte(goldenInterfaces), 0644); err != nil { t.Fatal(err) } - handler, err := NewHandler(tmpdir) + handler, err := NewHandler( + tmpdir, + &net.Interface{ + HardwareAddr: net.HardwareAddr([]byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}), + }, + &noopSink{}, + ) if err != nil { t.Fatal(err) } @@ -100,7 +116,7 @@ func TestLease(t *testing.T) { }, }, ) - handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + handler.serveDHCP(p, dhcp4.Request, p.ParseOptions()) if !leasedCalled { t.Fatalf("leased callback not called") } @@ -118,7 +134,7 @@ func TestPreferredAddress(t *testing.T) { t.Run("no requested IP", func(t *testing.T) { p := request(net.IPv4zero, hardwareAddr) - resp := handler.ServeDHCP(p, dhcp4.Discover, p.ParseOptions()) + resp := handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions()) if got, want := resp.YIAddr().To4(), addr.To4(); bytes.Equal(got, want) { t.Errorf("DHCPOFFER for wrong IP: got %v, want %v", got, want) } @@ -126,7 +142,7 @@ func TestPreferredAddress(t *testing.T) { t.Run("requested CIAddr", func(t *testing.T) { p := request(addr, hardwareAddr) - resp := handler.ServeDHCP(p, dhcp4.Discover, p.ParseOptions()) + resp := handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions()) if got, want := resp.YIAddr().To4(), addr.To4(); !bytes.Equal(got, want) { t.Errorf("DHCPOFFER for wrong IP: got %v, want %v", got, want) } @@ -151,7 +167,7 @@ func TestPreferredAddress(t *testing.T) { }, }, ) - resp := handler.ServeDHCP(p, dhcp4.Discover, p.ParseOptions()) + resp := handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions()) if got, want := resp.YIAddr().To4(), addr.To4(); !bytes.Equal(got, want) { t.Errorf("DHCPOFFER for wrong IP: got %v, want %v", got, want) } @@ -170,7 +186,7 @@ func TestPoolBoundaries(t *testing.T) { for _, last := range []byte{1, 242} { addr[len(addr)-1] = last p := request(addr, hardwareAddr) - resp := handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions()) opts := resp.ParseOptions() if got, want := dhcp4.MessageType(opts[dhcp4.OptionDHCPMessageType][0]), dhcp4.NAK; got != want { t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want) @@ -191,34 +207,34 @@ func TestPreviousLease(t *testing.T) { ) p := request(addr1, hardwareAddr1) - resp := handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions()) if got, want := resp.YIAddr().To4(), addr1.To4(); !bytes.Equal(got, want) { t.Errorf("DHCPREQUEST resulted in wrong IP: got %v, want %v", got, want) } p = request(addr1, hardwareAddr2) - resp = handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + resp = handler.serveDHCP(p, dhcp4.Request, p.ParseOptions()) opts := resp.ParseOptions() if got, want := dhcp4.MessageType(opts[dhcp4.OptionDHCPMessageType][0]), dhcp4.NAK; got != want { t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want) } p = discover(net.IPv4zero, hardwareAddr1) - resp = handler.ServeDHCP(p, dhcp4.Discover, p.ParseOptions()) + resp = handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions()) if got, want := resp.YIAddr().To4(), addr1.To4(); !bytes.Equal(got, want) { t.Errorf("DHCPOFFER for wrong IP: got %v, want %v", got, want) } // Free addr1 by requesting addr2 p = request(addr2, hardwareAddr1) - resp = handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + resp = handler.serveDHCP(p, dhcp4.Request, p.ParseOptions()) if got, want := resp.YIAddr().To4(), addr2.To4(); !bytes.Equal(got, want) { t.Errorf("DHCPREQUEST resulted in wrong IP: got %v, want %v", got, want) } // Verify addr1 is now available to other clients p = request(addr1, hardwareAddr2) - resp = handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + resp = handler.serveDHCP(p, dhcp4.Request, p.ParseOptions()) if got, want := resp.YIAddr().To4(), addr1.To4(); !bytes.Equal(got, want) { t.Errorf("DHCPREQUEST resulted in wrong IP: got %v, want %v", got, want) } @@ -244,7 +260,7 @@ func TestPermanentLease(t *testing.T) { }) p := request(addr, hardwareAddr) - resp := handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions()) if got, want := resp.YIAddr().To4(), addr.To4(); !bytes.Equal(got, want) { t.Errorf("DHCPREQUEST resulted in wrong IP: got %v, want %v", got, want) } @@ -254,7 +270,7 @@ func TestPermanentLease(t *testing.T) { hardwareAddr[len(hardwareAddr)-1] = 0x77 p = request(addr, hardwareAddr) - resp = handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + resp = handler.serveDHCP(p, dhcp4.Request, p.ParseOptions()) opts := resp.ParseOptions() if got, want := dhcp4.MessageType(opts[dhcp4.OptionDHCPMessageType][0]), dhcp4.NAK; got != want { t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want) @@ -278,7 +294,7 @@ func TestExpiration(t *testing.T) { addr[len(addr)-1] = byte(1 + (i % 254)) // avoid .0 (net) and .255 (broadcast) hardwareAddr[len(hardwareAddr)-1] = addr[len(addr)-1] p := request(addr, hardwareAddr) - resp := handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions()) if got, want := resp.YIAddr().To4(), addr.To4(); !bytes.Equal(got, want) { t.Errorf("DHCPREQUEST resulted in wrong IP: got %v, want %v", got, want) } @@ -291,7 +307,7 @@ func TestExpiration(t *testing.T) { addr[len(addr)-1] = byte(1 + (i % 254)) // avoid .0 (net) and .255 (broadcast) hardwareAddr[len(hardwareAddr)-1] = addr[len(addr)-1] p := request(addr, hardwareAddr) - resp := handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions()) if got, want := resp.YIAddr().To4(), addr.To4(); !bytes.Equal(got, want) { t.Errorf("DHCPREQUEST resulted in wrong IP: got %v, want %v", got, want) } @@ -304,7 +320,7 @@ func TestExpiration(t *testing.T) { addr[len(addr)-1] = byte(1 + (i % 254)) // avoid .0 (net) and .255 (broadcast) hardwareAddr[len(hardwareAddr)-1] = addr[len(addr)-1] - 1 p := request(addr, hardwareAddr) - resp := handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions()) opts := resp.ParseOptions() if got, want := dhcp4.MessageType(opts[dhcp4.OptionDHCPMessageType][0]), dhcp4.NAK; got != want { t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want) @@ -313,7 +329,7 @@ func TestExpiration(t *testing.T) { hardwareAddr[len(hardwareAddr)-1] = 0 p := discover(addr, hardwareAddr) - resp := handler.ServeDHCP(p, dhcp4.Discover, p.ParseOptions()) + resp := handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions()) if resp != nil { t.Errorf("DHCPDISCOVER(%v) resulted in unexpected offer of %v", addr, resp.YIAddr()) } @@ -326,7 +342,7 @@ func TestExpiration(t *testing.T) { for i := 1; i < 1+230; i++ { addr[len(addr)-1] = byte(1 + (i % 254)) // avoid .0 (net) and .255 (broadcast) p := request(addr, hardwareAddr) - resp := handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions()) if got, want := resp.YIAddr().To4(), addr.To4(); !bytes.Equal(got, want) { t.Errorf("DHCPREQUEST resulted in wrong IP: got %v, want %v", got, want) } @@ -347,7 +363,7 @@ func TestServerID(t *testing.T) { Code: dhcp4.OptionServerIdentifier, Value: net.IP{192, 168, 1, 1}, }) - resp := handler.ServeDHCP(p, dhcp4.Request, p.ParseOptions()) + resp := handler.serveDHCP(p, dhcp4.Request, p.ParseOptions()) if resp != nil { t.Errorf("DHCPDISCOVER(%v) resulted in unexpected offer of %v", addr, resp.YIAddr()) } @@ -371,7 +387,7 @@ func TestPersistentStorage(t *testing.T) { }) p := request(net.IPv4zero, hardwareAddr) - resp := handler.ServeDHCP(p, dhcp4.Discover, p.ParseOptions()) + resp := handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions()) if got, want := resp.YIAddr().To4(), addr.To4(); !bytes.Equal(got, want) { t.Errorf("DHCPOFFER for wrong IP: got %v, want %v", got, want) }