Files

1047 lines
28 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package dns implements a DNS forwarder.
package dns
import (
"bytes"
"cmp"
"context"
"encoding/json"
"errors"
"fmt"
"math"
"net"
"net/http"
"net/netip"
"net/url"
"os"
"slices"
"sort"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/rtr7/router7/internal/dhcp4d"
"github.com/rtr7/router7/internal/diag"
"github.com/rtr7/router7/internal/netconfig"
"github.com/rtr7/router7/internal/notify"
"github.com/rtr7/router7/internal/teelogger"
"github.com/miekg/dns"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promhttp"
"golang.org/x/net/idna"
"golang.org/x/time/rate"
)
var log = teelogger.NewConsole()
// lcHostname is a string type used for lower-cased hostnames so that the
// DHCP-based local name resolution can be made case-insensitive.
type lcHostname string
type IP struct {
IPv4 net.IP
IPv6 []net.IP
}
type DNS struct {
IP
Host string
}
type DNSClient struct {
udp *dns.Client
tcp *dns.Client
http *dohClient
dialer *net.Dialer
// TODO: Make cache either here or on Server. If on here we also need to allow bypassing the cache here...
}
var d net.Dialer
func DialContext(preResolved map[string]string) func(ctx context.Context, network, address string) (net.Conn, error) {
return func(ctx context.Context, network, address string) (net.Conn, error) {
if addr, ok := preResolved[address]; ok {
address = addr
}
return d.DialContext(ctx, network, address)
}
}
func NewDNSClient(dohMap map[string]string) *DNSClient {
if dohMap == nil {
dohMap = make(map[string]string)
}
return &DNSClient{
udp: &dns.Client{},
tcp: &dns.Client{Net: "tcp"},
http: &dohClient{
http: http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: DialContext(dohMap),
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
Timeout: 30 * time.Second,
},
},
}
}
func (d *DNSClient) Exchange(m *dns.Msg, address string, clientInfo map[string]string) (*dns.Msg, time.Duration, error) {
var (
in *dns.Msg
rtt time.Duration
err error
uri *url.URL
)
if strings.HasPrefix(address, "https") {
uri, err = url.Parse(address)
if err != nil {
return nil, -1, err
}
uri.Fragment = ""
values := uri.Query()
for key, value := range clientInfo {
values.Set(key, value)
}
uri.RawQuery = values.Encode()
in, rtt, err = d.http.Exchange(m, uri.String())
if err != nil {
return nil, -1, err // fall back to next-slower upstream
}
} else {
in, rtt, err = d.udp.Exchange(m, address)
if err != nil {
return nil, -1, err // fall back to next-slower upstream
}
}
if in.Truncated {
// Truncated response (exceeds UDP packet size), retry over TCP:
// https://www.rfc-editor.org/rfc/rfc2181#section-9
in, rtt, err = d.tcp.Exchange(m, address)
if err != nil {
return nil, -1, err
}
}
return in, rtt, nil
}
type Server struct {
Mux *dns.ServeMux
client *DNSClient
domains []string
sometimes *rate.Limiter
prom struct {
registry *prometheus.Registry
queries prometheus.Counter
upstream *prometheus.CounterVec
questions prometheus.Histogram
}
mu sync.Mutex
hostname string
ip IP
hostsByName map[lcHostname]IP
hostsByIP map[string]string // reverse ip notation -> hostname
subnames map[lcHostname]map[string]IP // hostname → subname → ip
aliases map[lcHostname]lcHostname
upstreamMu sync.RWMutex
upstream Upstreams
}
func Compact(ip IP) IP {
slices.SortFunc(ip.IPv6, func(ip1, ip2 net.IP) int {
return strings.Compare(ip1.String(), ip2.String())
})
ip.IPv6 = slices.CompactFunc(ip.IPv6, func(ip1, ip2 net.IP) bool {
return strings.Compare(ip1.String(), ip2.String()) == 0
})
return ip
}
func (ip IP) ToRRSet(name string, qtype uint16) ([]dns.RR, []dns.RR) {
var (
rr []dns.RR
re []dns.RR
r dns.RR
err error
)
if ip.IPv4 != nil {
r, err = dns.NewRR(name + " 3600 IN A " + ip.IPv4.String())
if err != nil {
panic(err)
}
if qtype == dns.TypeA {
rr = append(rr, r)
} else {
re = append(re, r)
}
}
slices.SortFunc(ip.IPv6, func(a, b net.IP) int {
// fe80 aka link local addresses will always sort last so we reverse the condition
// we want link local first so that ipv6 will resolve to local addresses especially
// since gokrazy only listens on private addresses
return cmp.Compare(a.String(), b.String())*-1
})
for _, IPv6 := range ip.IPv6 {
if IPv6 == nil {
continue
}
dnsStr := name + " 3600 IN AAAA " + IPv6.String()
r, err = dns.NewRR(dnsStr)
if err != nil {
fmt.Println("Failed to serialize ipv6 dns record:", dnsStr)
panic(err)
}
if qtype == dns.TypeAAAA {
rr = append(rr, r)
} else {
re = append(re, r)
}
}
return rr, re
}
func FindInterface(ip net.IP) (net.Interface, error) {
interfaces, err := net.Interfaces()
if err != nil {
return net.Interface{}, err
}
for _, iface := range interfaces {
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
ipnet, ok := addr.(*net.IPNet)
if !ok {
continue
}
if ipnet.IP.Equal(ip) {
return iface, nil
}
}
}
return net.Interface{}, errors.New("No ipv6 addr found")
}
var ipv6LinkLocal = diag.MustParseCIDR("fe80::/10")
func GetLinkLocalIPv6Address(ip string) net.IP {
iface, err := FindInterface(net.ParseIP(ip))
if err != nil {
return nil
}
addrs, err := iface.Addrs()
if err != nil {
return nil
}
var linkLocal netip.Addr
for _, addr := range addrs {
ipnet, ok := addr.(*net.IPNet)
if !ok {
continue
}
if ipv6LinkLocal.Contains(ipnet.IP) {
linkLocal, _ = netip.AddrFromSlice(ipnet.IP)
break
}
}
if linkLocal.IsValid() && !linkLocal.IsUnspecified() {
return net.IP(linkLocal.AsSlice())
}
return nil
}
type Upstreams struct {
Primary []string
Secondary []string
}
func NewServer(addr string, domains []string, upstream Upstreams) *Server {
if len(domains) == 0 {
domains = []string{"lan"}
}
for i := range domains {
domains[i] = strings.ToLower(domains[i])
}
hostname, _ := os.Hostname()
ip, _, _ := net.SplitHostPort(addr)
if len(upstream.Primary) < 1 {
upstream = Upstreams{Primary: []string{
// https://developers.google.com/speed/public-dns/docs/using#google_public_dns_ip_addresses
"8.8.8.8:53",
"8.8.4.4:53",
"[2001:4860:4860::8888]:53",
"[2001:4860:4860::8844]:53",
}}
}
dohMap := make(map[string]string)
for _, addr := range upstream.Primary {
if strings.HasPrefix("https", addr) {
uri, ip, found := strings.Cut(addr, "#")
if !found {
continue
}
parsedUri, err := url.Parse(uri)
if err != nil {
continue
}
dohMap[parsedUri.Host+":443"] = ip
}
}
server := &Server{
Mux: dns.NewServeMux(),
client: NewDNSClient(dohMap),
domains: domains,
upstream: upstream,
sometimes: rate.NewLimiter(rate.Every(1*time.Second), 1), // at most once per second
hostname: hostname,
ip: IP{
IPv4: net.ParseIP(ip),
IPv6: []net.IP{GetLinkLocalIPv6Address(ip)}, // TODO: IPv6 doesn't work for some reason
},
subnames: make(map[lcHostname]map[string]IP),
aliases: make(map[lcHostname]lcHostname),
}
server.prom.registry = prometheus.NewRegistry()
server.prom.queries = prometheus.NewCounter(prometheus.CounterOpts{
Name: "dns_queries",
Help: "Number of DNS queries received",
})
server.prom.registry.MustRegister(server.prom.queries)
server.prom.upstream = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "dns_upstream",
Help: "Which upstream answered which DNS query",
},
[]string{"upstream"},
)
server.prom.registry.MustRegister(server.prom.upstream)
server.prom.questions = prometheus.NewHistogram(prometheus.HistogramOpts{
Name: "dns_questions",
Help: "Number of questions in each DNS request",
Buckets: prometheus.LinearBuckets(0, 1, 10),
})
server.prom.registry.MustRegister(server.prom.questions)
server.prom.registry.MustRegister(collectors.NewGoCollector())
server.initHostsLocked()
server.Mux.HandleFunc(".", server.handleRequest)
server.Mux.HandleFunc("lan.", server.handleInternal)
server.Mux.HandleFunc("localhost.", server.handleInternal)
go func() {
for range time.Tick(10 * time.Second) {
server.probeUpstreamLatency()
}
}()
return server
}
func (s *Server) initHostsLocked() {
s.hostsByName = make(map[lcHostname]IP)
s.hostsByIP = make(map[string]string)
if s.hostname != "" && (s.ip.IPv4 != nil || len(s.ip.IPv6) > 0) {
lower := strings.ToLower(s.hostname)
s.hostsByName[lcHostname(lower)] = s.ip
if rev, err := dns.ReverseAddr(s.ip.IPv4.String()); err == nil {
s.hostsByIP[rev] = s.hostname
}
if rev, err := dns.ReverseAddr(s.ip.IPv6[0].String()); err == nil {
s.hostsByIP[rev] = s.hostname
}
s.Mux.HandleFunc(lower+".", s.subnameHandler(s.hostname))
for _, domain := range s.domains {
s.Mux.HandleFunc(lower+"."+domain+".", s.subnameHandler(s.hostname))
}
}
}
type measurement struct {
upstream string
rtt time.Duration
}
func (m measurement) String() string {
return fmt.Sprintf("{upstream: %s, rtt: %v}", m.upstream, m.rtt)
}
func (s *Server) probe(wg *sync.WaitGroup, upstreams []string, results []measurement) {
for idx, u := range upstreams {
wg.Add(1)
go func(idx int, u string) {
defer wg.Done()
// resolve a most-definitely cached record
m := new(dns.Msg)
m.SetQuestion("google.ch.", dns.TypeA)
start := time.Now()
_, _, err := s.client.Exchange(m, u, nil)
rtt := time.Since(start)
if err != nil {
// including unresponsive upstreams in results makes the update
// code simpler:
results[idx] = measurement{u, time.Duration(math.MaxInt64)}
return
}
results[idx] = measurement{u, rtt}
}(idx, u)
}
}
func (s *Server) probeUpstreamLatency() {
upstreams := s.upstreams()
primaryResults := make([]measurement, len(upstreams.Primary))
secondaryResults := make([]measurement, len(upstreams.Secondary))
var wg sync.WaitGroup
s.probe(&wg, upstreams.Primary, primaryResults)
s.probe(&wg, upstreams.Secondary, secondaryResults)
wg.Wait()
// Re-order by resolving latency:
sort.Slice(primaryResults, func(i, j int) bool {
return primaryResults[i].rtt < primaryResults[j].rtt
})
sort.Slice(secondaryResults, func(i, j int) bool {
return secondaryResults[i].rtt < secondaryResults[j].rtt
})
log.Printf("probe results: %v", append(primaryResults, secondaryResults...))
for idx, result := range primaryResults {
upstreams.Primary[idx] = result.upstream
}
for idx, result := range secondaryResults {
upstreams.Secondary[idx] = result.upstream
}
s.upstreamMu.Lock()
defer s.upstreamMu.Unlock()
s.upstream = upstreams
}
func (s *Server) hostByName(n string) (IP, bool) {
s.mu.Lock()
defer s.mu.Unlock()
r, ok := s.hostsByName[lcHostname(strings.ToLower(n))]
return r, ok
}
func (s *Server) hostByIP(n string) (string, bool) {
s.mu.Lock()
defer s.mu.Unlock()
r, ok := s.hostsByIP[n]
return r, ok
}
func (s *Server) subname(hostname, host string) (IP, bool) {
s.mu.Lock()
defer s.mu.Unlock()
r, ok := s.subnames[lcHostname(strings.ToLower(hostname))][host]
return r, ok
}
func (s *Server) PrometheusHandler() http.Handler {
return promhttp.HandlerFor(s.prom.registry, promhttp.HandlerOpts{})
}
func (s *Server) topLevelHandler(host string, w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
defer s.mu.Unlock()
remote, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
http.Error(w, fmt.Sprintf("net.SplitHostPort(%q): %v\n", r.RemoteAddr, err), http.StatusBadRequest)
return
}
rev, err := dns.ReverseAddr(remote)
if err != nil {
http.Error(w, fmt.Sprintf("dns.ReverseAddr(%v): %v\n", remote, err), http.StatusBadRequest)
return
}
hostname, ok := s.hostsByIP[rev]
if !ok {
err := fmt.Sprintf("connection without corresponding DHCP lease: %v\n", rev)
http.Error(w, err, http.StatusForbidden)
return
}
// This is a top level item we only allow a single label
host, _, _ = strings.Cut(strings.ToLower(host), ".")
ip, ok := s.hostsByName[lcHostname(strings.ToLower(hostname))]
if !ok {
http.Error(w, "Unable to find dhcp lease\n", http.StatusForbidden)
return
}
log.Printf("%s requesting dns %v -> %v", hostname, host, ip)
// Error if it looks like it's trying to take an existing dhcp lease
if existingIP, ok := s.hostsByName[lcHostname(host)]; ok && !ip.IPv4.Equal(existingIP.IPv4) {
http.Error(w, fmt.Sprintf("Host is alread set(%v): %v\n", existingIP, err), http.StatusBadRequest)
return
}
s.aliases[lcHostname(host)] = lcHostname(strings.ToLower(hostname))
log.Printf("Alias set %s -> %s", host, strings.ToLower(hostname))
s.Mux.HandleFunc(host+".", s.subnameHandler(host))
for _, domain := range s.domains {
s.Mux.HandleFunc(host+"."+domain+".", s.subnameHandler(host))
}
w.Write([]byte("ok\n"))
}
func (s *Server) IPv6Handler(w http.ResponseWriter, r *http.Request) {
// ipv6 sucks there isn't a clear way to get the IP
// this only listens on private addresses, but we need public IPs
port, err := strconv.Atoi(r.URL.Query().Get("port"))
if port < 0 || err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "Invalid port recieved %v: %v\n", r.URL.Query().Get("port"), err)
return
}
addr, err := netip.ParseAddrPort(r.RemoteAddr)
if err != nil {
http.Error(w, "invalid ip", http.StatusBadRequest)
return
}
remoteIP, _ := dns.ReverseAddr(addr.Addr().String())
host, ok := s.hostByIP(remoteIP)
if !ok {
http.Error(w, "Your IP is not in dns yet. Try again later", http.StatusBadRequest)
}
dnsIP, ok := s.hostsByName[lcHostname(strings.ToLower(host))]
if !ok {
http.Error(w, "Somehow your hostname is not in dns yet. Try again later", http.StatusBadRequest)
}
// Hardcode /perm because otherwise it's more work
b, _ := os.ReadFile("/perm/ipv6.portforwardings.json")
// We don't actually care about the error because we will be adding to it
var cfg netconfig.IPv6PortForwardings
json.Unmarshal(b, &cfg)
// we also don't care about this error as we will be writing it in a second
var ipv6 netip.Addr
for _, ip := range dnsIP.IPv6 {
ipv6, ok = netip.AddrFromSlice(ip)
if !ok {
ipv6 = netip.Addr{}
continue
}
if ipv6.IsGlobalUnicast() && !ipv6.IsPrivate() {
break
}
ipv6 = netip.Addr{}
}
if ipv6 == (netip.Addr{}) {
http.Error(w, "Unable to find ipv6 address for host. Try again later", 512)
return
}
cfg.IPv6PortForwardings = append(cfg.IPv6PortForwardings, netip.AddrPortFrom(ipv6, uint16(port)))
buf := &bytes.Buffer{}
j := json.NewEncoder(buf)
j.SetIndent("", " ")
err = j.Encode(cfg)
if err != nil {
http.Error(w, "Process request. Try again later though it probably won't help", http.StatusInternalServerError)
return
}
err = os.WriteFile("/perm/ipv6.portforwardings.json", buf.Bytes(), 0o600)
if err != nil {
http.Error(w, "Process request. Try again later though it probably won't help", http.StatusInternalServerError)
return
}
// Notify netconfigd of ipv6 address needing a port forward
notify.Process("/user/netconfigd", syscall.SIGUSR1)
}
func (s *Server) DyndnsHandler(w http.ResponseWriter, r *http.Request) {
host := r.FormValue("host")
ips := r.FormValue("ip")
if ips == "" || strings.HasPrefix(r.RemoteAddr, ips+":") {
s.topLevelHandler(host, w, r)
return
}
ip := net.ParseIP(ips)
if ip == nil {
http.Error(w, "invalid ip", http.StatusBadRequest)
return
}
s.mu.Lock()
defer s.mu.Unlock()
remote, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
http.Error(w, fmt.Sprintf("net.SplitHostPort(%q): %v", r.RemoteAddr, err), http.StatusBadRequest)
return
}
rev, err := dns.ReverseAddr(remote)
if err != nil {
http.Error(w, fmt.Sprintf("dns.ReverseAddr(%v): %v", remote, err), http.StatusBadRequest)
return
}
hostname, ok := s.hostsByIP[rev]
if !ok {
err := fmt.Sprintf("connection without corresponding DHCP lease: %v", rev)
http.Error(w, err, http.StatusForbidden)
return
}
lower := strings.ToLower(hostname)
log.Printf("%s requesting dns %v -> %v", lower, host, ip)
subnames, ok := s.subnames[lcHostname(lower)]
if !ok {
subnames = make(map[string]IP)
s.subnames[lcHostname(lower)] = subnames
}
if ip.To4() != nil {
subnames[host] = IP{
IPv4: ip,
IPv6: subnames[host].IPv6,
}
} else {
subnames[host] = Compact(IP{
IPv4: subnames[host].IPv4,
IPv6: append(subnames[host].IPv6, ip),
})
}
w.Write([]byte("ok\n"))
}
func ipType(ip net.IP) int {
if ip.To4() == nil {
return 6
}
return 4
}
func (s *Server) SetLeases(leases []dhcp4d.Lease) {
s.mu.Lock()
defer s.mu.Unlock()
s.initHostsLocked()
now := time.Now()
{
// defensive copy
slice := make([]dhcp4d.Lease, len(leases))
copy(slice, leases)
leases = slice
}
// First entry wins, so we order by expiration descendingly to put the
// newest entry for any given name into s.hostsByName.
sort.SliceStable(leases, func(i, j int) bool {
return !leases[i].Expiry.Before(leases[j].Expiry)
})
for _, l := range leases {
if l.Expired(now) {
continue
}
if l.Hostname == "" {
continue
}
lower := strings.ToLower(l.Hostname)
if ip, ok := s.hostsByName[lcHostname(lower)]; ok && (ipType(l.Addr) == 4 && ip.IPv4 != nil) {
continue // don’t overwrite e.g. the hostname entry, but allow a missing ipv4, ipv6 gets appended
}
_, err := idna.Registration.ToASCII(lower)
if err != nil && !strings.Contains(lower, " ") {
log.Println("Unable to register", l.Hostname, "in dns", err)
continue
}
if l.Addr.To4() != nil { // even though this is a dhcpv4 lease we reuse it for ipv6 addresses
s.hostsByName[lcHostname(lower)] = IP{
IPv4: l.Addr,
IPv6: s.hostsByName[lcHostname(lower)].IPv6,
}
} else {
s.hostsByName[lcHostname(lower)] = Compact(IP{
IPv4: s.hostsByName[lcHostname(lower)].IPv4,
IPv6: append(s.hostsByName[lcHostname(lower)].IPv6, l.Addr),
})
}
if rev, err := dns.ReverseAddr(l.Addr.String()); err == nil {
s.hostsByIP[rev] = l.Hostname
}
s.Mux.HandleFunc(lower+".", s.subnameHandler(lower))
for _, domain := range s.domains {
s.Mux.HandleFunc(lower+"."+domain+".", s.subnameHandler(lower))
}
}
}
var localNets = []*net.IPNet{
// loopback: https://tools.ietf.org/html/rfc3330#section-2
diag.MustParseCIDR("127.0.0.0/8"),
// loopback: https://tools.ietf.org/html/rfc3513#section-2.4
diag.MustParseCIDR("::1/128"),
// reversed: https://tools.ietf.org/html/rfc1918#section-3
diag.MustParseCIDR("10.0.0.0/8"),
diag.MustParseCIDR("172.16.0.0/12"),
diag.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
}
var errEmpty = errors.New("no answers")
var (
l4, _ = dns.NewRR("localhost. 3600 IN A 127.0.0.1")
l6, _ = dns.NewRR("localhost. 3600 IN AAAA ::1")
)
func (s *Server) SetDNSEntries(dnsEntries []DNS) {
for _, entry := range dnsEntries {
hostname := strings.TrimRight(strings.ToLower(entry.Host), ".")
handler := func(w dns.ResponseWriter, r *dns.Msg) {
if len(r.Question) != 1 {
return
}
q := r.Question[0]
// I don't feel like making it apply for all domains
// I can always write out the domain in the config file
if lower := strings.ToLower(q.Name); lower == hostname+"." || lower == hostname+"."+s.domains[0]+"." {
rr, re := entry.ToRRSet(q.Name, q.Qtype)
if len(rr) < 1 {
m := new(dns.Msg)
m.SetReply(r)
m.Extra = append(m.Extra, re...)
m.RecursionAvailable = true
w.WriteMsg(m)
return
}
m := new(dns.Msg)
m.SetReply(r)
m.RecursionAvailable = true
m.Answer = append(m.Answer, rr...)
m.Extra = append(m.Extra, re...)
w.WriteMsg(m)
return
}
log.Printf("This shouldn't happen: dns entry %q: dns Query: %s", hostname, r)
// Send an authoritative NXDOMAIN for local names:
m := new(dns.Msg)
m.RecursionAvailable = true
m.SetReply(r)
m.SetRcode(r, dns.RcodeNameError)
w.WriteMsg(m)
}
s.Mux.HandleFunc(hostname+".", handler)
for _, domain := range s.domains {
s.Mux.HandleFunc(hostname+"."+domain+".", handler)
}
}
}
func (s *Server) resolve(q dns.Question) (rr []dns.RR, re []dns.RR, err error) {
if q.Qclass != dns.ClassINET {
return nil, nil, nil
}
if strings.ToLower(q.Name) == "localhost." {
if q.Qtype == dns.TypeA {
rr = append(rr, l4)
re = append(re, l6)
} else {
rr = append(rr, l6)
re = append(re, l4)
}
return rr, re, nil
}
if q.Qtype == dns.TypeA ||
q.Qtype == dns.TypeAAAA ||
q.Qtype == dns.TypeMX {
name := strings.TrimSuffix(q.Name, ".")
for _, domain := range s.domains {
name = strings.TrimSuffix(name, "."+domain)
}
if ip, ok := s.hostByName(name); ok {
r, e := ip.ToRRSet(q.Name, q.Qtype)
rr = append(rr, r...)
re = append(re, e...)
if len(rr) == 0 {
return nil, nil, errEmpty
}
return rr, re, nil
}
if ip, ok := s.hostByName(string(s.aliases[lcHostname(name)])); ok {
r, e := ip.ToRRSet(q.Name, q.Qtype)
rr = append(rr, r...)
re = append(re, e...)
if len(rr) == 0 {
return nil, nil, errEmpty
}
return rr, re, nil
}
}
if q.Qtype == dns.TypePTR {
if host, ok := s.hostByIP(q.Name); ok {
r, err := dns.NewRR(q.Name + " 3600 IN PTR " + host + "." + s.domains[0])
if err == nil {
rr = append(rr, r)
return rr, re, nil
}
log.Println("Failed to create reverse record for host: ", host)
return nil, nil, nil
}
if strings.HasSuffix(q.Name, "127.in-addr.arpa.") {
r, err := dns.NewRR(q.Name + " 3600 IN PTR localhost.")
if err != nil {
return nil, nil, err
}
rr = append(rr, r)
return rr, re, nil
}
}
return nil, nil, nil
}
func (s *Server) handleInternal(w dns.ResponseWriter, r *dns.Msg) {
s.prom.queries.Inc()
s.prom.questions.Observe(float64(len(r.Question)))
s.prom.upstream.WithLabelValues("local").Inc()
if len(r.Question) != 1 { // TODO: answer all questions we can answer
return
}
rr, re, err := s.resolve(r.Question[0])
if err != nil {
if err == errEmpty {
m := new(dns.Msg)
m.SetReply(r)
m.Extra = append(m.Extra, re...)
m.RecursionAvailable = true
w.WriteMsg(m)
return
}
log.Fatal("Failed to resolve internal host", err)
}
if len(rr) > 0 {
m := new(dns.Msg)
m.SetReply(r)
m.RecursionAvailable = true
m.Answer = append(m.Answer, rr...)
m.Extra = append(m.Extra, re...)
w.WriteMsg(m)
return
}
// Send an authoritative NXDOMAIN for local names:
m := new(dns.Msg)
m.SetReply(r)
m.RecursionAvailable = true
m.SetRcode(r, dns.RcodeNameError)
w.WriteMsg(m)
return
}
func (s *Server) upstreams() Upstreams {
s.upstreamMu.RLock()
defer s.upstreamMu.RUnlock()
result := Upstreams{}
result.Primary = make([]string, len(s.upstream.Primary))
result.Secondary = make([]string, len(s.upstream.Secondary))
copy(result.Primary, s.upstream.Primary)
copy(result.Secondary, s.upstream.Secondary)
return result
}
func (s *Server) updateUpstreams(u string) {
s.upstreamMu.Lock()
defer s.upstreamMu.Unlock()
if idx := slices.Index(s.upstream.Primary, u); idx > 0 {
s.upstream.Primary = append(append([]string{u}, s.upstream.Primary[:idx]...), s.upstream.Primary[idx+1:]...)
return
}
if idx := slices.Index(s.upstream.Secondary, u); idx > 0 {
s.upstream.Secondary = append(append([]string{u}, s.upstream.Secondary[:idx]...), s.upstream.Secondary[idx+1:]...)
return
}
}
func (s *Server) handleRequest(w dns.ResponseWriter, r *dns.Msg) {
if len(r.Question) == 1 { // TODO: answer all questions we can answer
q := r.Question[0]
if q.Qtype == dns.TypePTR && q.Qclass == dns.ClassINET && isLocalInAddrArpa(q.Name) {
s.handleInternal(w, r)
return
}
}
s.prom.queries.Inc()
s.prom.questions.Observe(float64(len(r.Question)))
s.prom.upstream.WithLabelValues("DNS").Inc()
var (
err error
clientInfo = make(map[string]string) // device_id=12345&device_name=John's%20Firefox&device_model=iPhone
// TODO: make device_id and make it compatible with headscale.
// TODO: extract device_model from dhcp vendor identifier
)
if addr := w.RemoteAddr(); addr != nil {
if ip, _, ok := strings.Cut(w.RemoteAddr().String(), ":"); ok {
ip, _ = dns.ReverseAddr(ip)
host, ok := s.hostByIP(ip)
if ok {
clientInfo["device_name"] = host
}
}
}
upstreams := s.upstreams()
// We have primary and secondary so we can ensure that nextdns doh is tried first
for idx, u := range append(upstreams.Primary, upstreams.Secondary...) {
var in *dns.Msg
in, _, err = s.client.Exchange(r, u, clientInfo)
if err != nil {
if s.sometimes.Allow() {
log.Printf("resolving %v failed: %v", r.Question, err)
}
continue // fall back to next-slower upstream
}
w.WriteMsg(in)
if idx > 0 {
s.updateUpstreams(u)
}
return
}
// DNS has no reply for resolving errors
}
func (s *Server) resolveSubname(hostname string, q dns.Question, w dns.ResponseWriter, r *dns.Msg) ([]dns.RR, []dns.RR, error) {
if q.Qclass != dns.ClassINET {
return nil, nil, nil
}
if q.Qtype == dns.TypeA ||
q.Qtype == dns.TypeAAAA ||
q.Qtype == dns.TypeMX {
name := strings.TrimSuffix(q.Name, ".")
for _, domain := range s.domains {
name = strings.TrimSuffix(name, "."+domain)
}
if lower := strings.ToLower(name); lower == hostname {
host, ok := s.hostByName(hostname)
if !ok {
if ip, ok := s.hostByName(string(s.aliases[lcHostname(hostname)])); ok {
r, e := ip.ToRRSet(q.Name, q.Qtype)
if len(r) == 0 {
return nil, nil, errEmpty
}
return r, e, nil
}
// The corresponding DHCP lease might have expired, but this
// handler is still installed on the mux.
return nil, nil, nil // NXDOMAIN
}
if rr, re := host.ToRRSet(q.Name, q.Qtype); len(rr) > 0 {
return rr, re, nil
}
return nil, nil, errEmpty
}
name = strings.TrimSuffix(name, "."+hostname)
if ip, ok := s.subname(hostname, name); ok {
rr, re := ip.ToRRSet(q.Name, q.Qtype)
if len(rr) > 0 {
return rr, re, nil
}
return nil, nil, errEmpty
}
// Here we have failed to find a name in our lan. Because there's so many TLDs eg "cloud" it fucks with things.
// So if we have an alias with a subname eg hetzner.cloud where cloud is an alias we want to send it to an upstream
if _, alias := s.aliases[lcHostname(hostname)]; alias && name != hostname && strings.HasSuffix(q.Name, hostname+".") {
s.handleRequest(w, r)
return nil, nil, ErrHandled
}
}
return nil, nil, nil
}
var ErrHandled = errors.New("Handled")
func (s *Server) subnameHandler(hostname string) func(w dns.ResponseWriter, r *dns.Msg) {
return func(w dns.ResponseWriter, r *dns.Msg) {
if len(r.Question) != 1 { // TODO: answer all questions we can answer
return
}
rr, re, err := s.resolveSubname(hostname, r.Question[0], w, r)
if err != nil {
if err == errEmpty {
m := new(dns.Msg)
m.SetReply(r)
m.RecursionAvailable = true
m.Extra = append(m.Extra, re...)
w.WriteMsg(m)
return
}
if errors.Is(err, ErrHandled) {
return
}
log.Fatalf("question %#v: %v", r.Question[0], err)
}
if len(rr) > 0 {
m := new(dns.Msg)
m.SetReply(r)
m.RecursionAvailable = true
m.Answer = append(m.Answer, rr...)
m.Extra = append(m.Extra, re...)
w.WriteMsg(m)
return
}
// Send an authoritative NXDOMAIN for local names:
m := new(dns.Msg)
m.RecursionAvailable = true
m.SetReply(r)
m.Extra = append(m.Extra, re...)
m.SetRcode(r, dns.RcodeNameError)
w.WriteMsg(m)
}
}