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:
Michael Stapelberg 2018-06-23 14:11:59 +02:00
parent 301d4c0d00
commit 3ba84074c7
3 changed files with 100 additions and 27 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}