diff --git a/internal/dns/dns.go b/internal/dns/dns.go index 88a9a62..f8dd456 100644 --- a/internal/dns/dns.go +++ b/internal/dns/dns.go @@ -2,6 +2,7 @@ package dns import ( "log" + "net" "strings" "time" @@ -45,6 +46,50 @@ func (s *Server) SetLeases(leases []dhcp4d.Lease) { } } +func mustParseCIDR(s string) *net.IPNet { + _, ipnet, err := net.ParseCIDR(s) + if err != nil { + panic(err) + } + return ipnet +} + +var ( + localNets = []*net.IPNet{ + // reversed: https://tools.ietf.org/html/rfc1918#section-3 + mustParseCIDR("10.0.0.0/8"), + mustParseCIDR("172.16.0.0/12"), + mustParseCIDR("192.168.0.0/16"), + } +) + +func reverse(ss []string) { + last := len(ss) - 1 + for i := 0; i < len(ss)/2; i++ { + ss[i], ss[last-i] = ss[last-i], ss[i] + } +} + +func isLocalInAddrArpa(q string) bool { + if !strings.HasSuffix(q, ".in-addr.arpa.") { + return false + } + parts := strings.Split(strings.TrimSuffix(q, ".in-addr.arpa."), ".") + reverse(parts) + ip := net.ParseIP(strings.Join(parts, ".")) + if ip == nil { + return false + } + var local bool + for _, l := range localNets { + if l.Contains(ip) { + local = true + break + } + } + return local +} + // TODO: is handleRequest called in more than one goroutine at a time? // TODO: require search domains to be present, then use HandleFunc("lan.", internalName) func (s *Server) handleRequest(w dns.ResponseWriter, r *dns.Msg) { @@ -69,7 +114,7 @@ func (s *Server) handleRequest(w dns.ResponseWriter, r *dns.Msg) { } } if q.Qtype == dns.TypePTR && q.Qclass == dns.ClassINET { - if strings.HasSuffix(q.Name, "168.192.in-addr.arpa.") { + if isLocalInAddrArpa(q.Name) { if host, ok := s.hostsByIP[q.Name]; ok { rr, err := dns.NewRR(q.Name + " 3600 IN PTR " + host + "." + s.domain) if err != nil { diff --git a/internal/dns/dns_test.go b/internal/dns/dns_test.go index a686ece..63d2356 100644 --- a/internal/dns/dns_test.go +++ b/internal/dns/dns_test.go @@ -75,26 +75,53 @@ func TestDHCP(t *testing.T) { } func TestDHCPReverse(t *testing.T) { - r := &recorder{} - s := NewServer("localhost:0", "lan") - s.SetLeases([]dhcp4d.Lease{ + for _, test := range []struct { + ip net.IP + question string + }{ { - Hostname: "xps", - Addr: net.IP{192, 168, 42, 23}, + ip: net.IP{192, 168, 42, 23}, + question: "23.42.168.192.in-addr.arpa.", }, - }) - m := new(dns.Msg) - m.SetQuestion("23.42.168.192.in-addr.arpa.", dns.TypePTR) - s.handleRequest(r, m) - if got, want := len(r.response.Answer), 1; got != want { - t.Fatalf("unexpected number of answers: got %d, want %d", got, want) - } - a := r.response.Answer[0] - if _, ok := a.(*dns.PTR); !ok { - t.Fatalf("unexpected response type: got %T, want dns.A", a) - } - if got, want := a.(*dns.PTR).Ptr, "xps.lan."; got != want { - t.Fatalf("unexpected response record: got %q, want %q", got, want) + + { + ip: net.IP{10, 0, 0, 2}, + question: "2.0.0.10.in-addr.arpa.", + }, + + { + ip: net.IP{172, 16, 0, 1}, // 172.16/12 HostMin + question: "1.0.16.172.in-addr.arpa.", + }, + + { + ip: net.IP{172, 31, 255, 254}, // 172.16/12 HostMax + question: "254.255.31.172.in-addr.arpa.", + }, + } { + t.Run(test.question, func(t *testing.T) { + r := &recorder{} + s := NewServer("localhost:0", "lan") + s.SetLeases([]dhcp4d.Lease{ + { + Hostname: "xps", + Addr: test.ip, + }, + }) + m := new(dns.Msg) + m.SetQuestion(test.question, dns.TypePTR) + s.handleRequest(r, m) + if got, want := len(r.response.Answer), 1; got != want { + t.Fatalf("unexpected number of answers: got %d, want %d", got, want) + } + a := r.response.Answer[0] + if _, ok := a.(*dns.PTR); !ok { + t.Fatalf("unexpected response type: got %T, want dns.A", a) + } + if got, want := a.(*dns.PTR).Ptr, "xps.lan."; got != want { + t.Fatalf("unexpected response record: got %q, want %q", got, want) + } + }) } }