From 53c495091e0a152df0e97e32c414b7815e88f3f2 Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Sat, 23 May 2020 09:07:17 +0200 Subject: [PATCH] quirk: enforce minimum lease time of 1 hour for Nintendo devices The Nintendo Switch has been observed to hold on to IP addresses even after their expiration. My guess is that this is an oversight: likely the device enters power saving mode with a configured IP address and just sleeps through the expiration time. As the device seems to wake up once every hour, we enforce a minimum lease time of 1 hour, but only for affected devices. The rest of the network gets short lease times. https://twitter.com/zekjur/status/1263949112036282374 --- internal/dhcp4d/dhcp4d.go | 36 ++++++++++++++--- internal/dhcp4d/dhcp4d_test.go | 45 +++++++++++++++++++++ internal/dhcp4d/quirks.go | 73 ++++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 internal/dhcp4d/quirks.go diff --git a/internal/dhcp4d/dhcp4d.go b/internal/dhcp4d/dhcp4d.go index a7d3a00..9cadf96 100644 --- a/internal/dhcp4d/dhcp4d.go +++ b/internal/dhcp4d/dhcp4d.go @@ -16,9 +16,13 @@ package dhcp4d import ( + "bytes" + "encoding/hex" "log" "math/rand" "net" + "sort" + "strings" "sync" "syscall" "time" @@ -246,17 +250,32 @@ func (h *Handler) leaseHW(hwAddr string) (*Lease, bool) { return l, ok && l.HardwareAddr == hwAddr } +func (h *Handler) leasePeriodForDevice(hwAddr string) time.Duration { + hwAddrPrefix, err := hex.DecodeString(strings.ReplaceAll(hwAddr, ":", "")) + if err != nil { + return h.LeasePeriod + } + hwAddrPrefix = hwAddrPrefix[:3] + i := sort.Search(len(nintendoMacPrefixes), func(i int) bool { + return bytes.Compare(nintendoMacPrefixes[i][:], hwAddrPrefix) >= 0 + }) + if i < len(nintendoMacPrefixes) && bytes.Equal(nintendoMacPrefixes[i][:], hwAddrPrefix) { + return 1 * time.Hour + } + return h.LeasePeriod +} + // 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()) } + hwAddr := p.CHAddr().String() switch msgType { case dhcp4.Discover: free := -1 - hwAddr := p.CHAddr().String() // try to offer the requested IP, if any and available if !reqIP.To4().Equal(net.IPv4zero) { @@ -284,14 +303,14 @@ func (h *Handler) serveDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options d dhcp4.Offer, h.serverIP, dhcp4.IPAdd(h.start, free), - h.LeasePeriod, + h.leasePeriodForDevice(hwAddr), h.options.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])) case dhcp4.Request: if server, ok := options[dhcp4.OptionServerIdentifier]; ok && !net.IP(server).Equal(h.serverIP) { return nil // message not for this dhcp server } - leaseNum := h.canLease(reqIP, p.CHAddr().String()) + leaseNum := h.canLease(reqIP, hwAddr) if leaseNum == -1 { return dhcp4.ReplyPacket(p, dhcp4.NAK, h.serverIP, nil, 0, nil) } @@ -299,8 +318,8 @@ func (h *Handler) serveDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options d lease := &Lease{ Num: leaseNum, Addr: make([]byte, 4), - HardwareAddr: p.CHAddr().String(), - Expiry: h.timeNow().Add(h.LeasePeriod), + HardwareAddr: hwAddr, + Expiry: h.timeNow().Add(h.leasePeriodForDevice(hwAddr)), Hostname: string(options[dhcp4.OptionHostName]), } copy(lease.Addr, reqIP.To4()) @@ -327,7 +346,12 @@ func (h *Handler) serveDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options d h.leasesIP[leaseNum] = lease h.leasesHW[lease.HardwareAddr] = leaseNum h.callLeasesLocked(lease) - return dhcp4.ReplyPacket(p, dhcp4.ACK, h.serverIP, reqIP, h.LeasePeriod, + return dhcp4.ReplyPacket( + p, + dhcp4.ACK, + h.serverIP, + reqIP, + h.leasePeriodForDevice(hwAddr), h.options.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])) } return nil diff --git a/internal/dhcp4d/dhcp4d_test.go b/internal/dhcp4d/dhcp4d_test.go index 522f2dd..58c85ec 100644 --- a/internal/dhcp4d/dhcp4d_test.go +++ b/internal/dhcp4d/dhcp4d_test.go @@ -15,6 +15,7 @@ package dhcp4d import ( + "encoding/binary" "io/ioutil" "net" "os" @@ -455,3 +456,47 @@ func TestPersistentStorage(t *testing.T) { t.Errorf("DHCPOFFER for wrong IP: got %v, want %v", got, want) } } + +func TestMinimumLeaseTime(t *testing.T) { + handler, cleanup := testHandler(t) + defer cleanup() + + var addr = net.IP{192, 168, 42, 23} + + for _, tt := range []struct { + hwaddr net.HardwareAddr + wantLeaseTime time.Duration + }{ + { + hwaddr: net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}, + wantLeaseTime: 20 * time.Minute, + }, + + { + // Nintendo MAC address range for Nintendo Switch specific minimum + // lease time quirk: + hwaddr: net.HardwareAddr{0x7c, 0xbb, 0x8a, 0x11, 0x22, 0x33}, + wantLeaseTime: 1 * time.Hour, + }, + } { + p := discover(addr, tt.hwaddr) + resp := handler.serveDHCP(p, dhcp4.Discover, p.ParseOptions()) + if resp == nil { + t.Fatalf("DHCPDISCOVER(%v) = nil", tt.hwaddr) + } + if got, want := messageType(resp), dhcp4.Offer; got != want { + t.Errorf("DHCPDISCOVER resulted in unexpected message type: got %v, want %v", got, want) + } + + opts := resp.ParseOptions() + leaseTimeBytes, ok := opts[dhcp4.OptionIPAddressLeaseTime] + if !ok { + t.Fatalf("DHCPDISCOVER(%v): lease time option not set", tt.hwaddr) + } + leaseTimeSecs := binary.BigEndian.Uint32(leaseTimeBytes) + want := uint32(tt.wantLeaseTime.Seconds()) + if got := leaseTimeSecs; got != want { + t.Errorf("unexpected lease time for hwaddr %v: got %d, want %d", tt.hwaddr, got, want) + } + } +} diff --git a/internal/dhcp4d/quirks.go b/internal/dhcp4d/quirks.go new file mode 100644 index 0000000..e659ebe --- /dev/null +++ b/internal/dhcp4d/quirks.go @@ -0,0 +1,73 @@ +package dhcp4d + +// Sorted list of MAC address prefixes assigned to Nintendo. +// From the IEEE MA-L (MAC Address Block Large, formerly known as OUI) database. +var nintendoMacPrefixes = [...][3]byte{ + {0x0, 0x9, 0xbf}, + {0x0, 0x16, 0x56}, + {0x0, 0x17, 0xab}, + {0x0, 0x19, 0x1d}, + {0x0, 0x19, 0xfd}, + {0x0, 0x1a, 0xe9}, + {0x0, 0x1b, 0x7a}, + {0x0, 0x1b, 0xea}, + {0x0, 0x1c, 0xbe}, + {0x0, 0x1d, 0xbc}, + {0x0, 0x1e, 0x35}, + {0x0, 0x1e, 0xa9}, + {0x0, 0x1f, 0x32}, + {0x0, 0x1f, 0xc5}, + {0x0, 0x21, 0x47}, + {0x0, 0x21, 0xbd}, + {0x0, 0x22, 0x4c}, + {0x0, 0x22, 0xaa}, + {0x0, 0x22, 0xd7}, + {0x0, 0x23, 0x31}, + {0x0, 0x23, 0xcc}, + {0x0, 0x24, 0x1e}, + {0x0, 0x24, 0x44}, + {0x0, 0x24, 0xf3}, + {0x0, 0x25, 0xa0}, + {0x0, 0x26, 0x59}, + {0x0, 0x27, 0x9}, + {0x4, 0x3, 0xd6}, + {0x18, 0x2a, 0x7b}, + {0x2c, 0x10, 0xc1}, + {0x34, 0x2f, 0xbd}, + {0x34, 0xaf, 0x2c}, + {0x40, 0xd2, 0x8a}, + {0x40, 0xf4, 0x7}, + {0x48, 0xa5, 0xe7}, + {0x58, 0x2f, 0x40}, + {0x58, 0xbd, 0xa3}, + {0x5c, 0x52, 0x1e}, + {0x60, 0x6b, 0xff}, + {0x64, 0xb5, 0xc6}, + {0x70, 0x48, 0xf7}, + {0x78, 0xa2, 0xa0}, + {0x7c, 0xbb, 0x8a}, + {0x8c, 0x56, 0xc5}, + {0x8c, 0xcd, 0xe8}, + {0x94, 0x58, 0xcb}, + {0x98, 0x41, 0x5c}, + {0x98, 0xb6, 0xe9}, + {0x98, 0xe8, 0xfa}, + {0x9c, 0xe6, 0x35}, + {0xa4, 0x38, 0xcc}, + {0xa4, 0x5c, 0x27}, + {0xa4, 0xc0, 0xe1}, + {0xb8, 0x78, 0x26}, + {0xb8, 0x8a, 0xec}, + {0xb8, 0xae, 0x6e}, + {0xcc, 0x9e, 0x0}, + {0xcc, 0xfb, 0x65}, + {0xd4, 0xf0, 0x57}, + {0xd8, 0x6b, 0xf7}, + {0xdc, 0x68, 0xeb}, + {0xe0, 0xc, 0x7f}, + {0xe0, 0xe7, 0x51}, + {0xe0, 0xf6, 0xb5}, + {0xe8, 0x4e, 0xce}, + {0xe8, 0xda, 0x20}, + {0xec, 0xc4, 0xd}, +}