dhcp4d: send replies as unicast using raw sockets
Preferring unicast over multicast (which hogs a lot of airtime on WiFi networks) is a best practice. Some device/access point vendor combinations even seem to entirely eat (some?) broadcast traffic (sometimes), e.g. my Apple iPhone SE/Ubiquiti UAP-AC-HD, so that using unicast is required for reliable WiFi.
This commit is contained in:
parent
301d4c0d00
commit
3ba84074c7
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user