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
This commit is contained in:
Michael Stapelberg 2020-05-23 09:07:17 +02:00
parent d81b77a876
commit 53c495091e
3 changed files with 148 additions and 6 deletions

View File

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

View File

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

73
internal/dhcp4d/quirks.go Normal file
View File

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