From e369a9484a74900dedd92f866f944f0e84a3d740 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 15 Jul 2020 08:53:55 +0200 Subject: [PATCH 1/2] Improve captive portal handling --- firewall/master.go | 13 +++++ nameserver/nameserver.go | 2 +- netenv/environment_darwin.go | 2 +- netenv/environment_linux.go | 10 ++-- netenv/environment_windows.go | 88 +++++++++++++++++++++++++++++-- netenv/online-status.go | 99 +++++++++++++++++++++++++++++------ netenv/online-status_test.go | 2 +- network/connection.go | 9 ++++ resolver/resolve.go | 8 +-- resolver/resolver-env.go | 95 +++++++++++++++++++++++++++++++++ resolver/resolver.go | 2 + resolver/resolvers.go | 11 +++- resolver/scopes.go | 13 +++++ 13 files changed, 323 insertions(+), 31 deletions(-) create mode 100644 resolver/resolver-env.go diff --git a/firewall/master.go b/firewall/master.go index 7f194960..c31fdbf6 100644 --- a/firewall/master.go +++ b/firewall/master.go @@ -7,6 +7,8 @@ import ( "path/filepath" "strings" + "github.com/safing/portmaster/netenv" + "github.com/safing/portbase/log" "github.com/safing/portmaster/network" "github.com/safing/portmaster/network/netutils" @@ -50,6 +52,7 @@ func DecideOnConnection(ctx context.Context, conn *network.Connection, pkt packe checkSelfCommunication, checkProfileExists, checkConnectionType, + checkCaptivePortal, checkConnectionScope, checkEndpointLists, checkBypassPrevention, @@ -178,6 +181,16 @@ func checkConnectionType(ctx context.Context, conn *network.Connection, _ packet return false } +func checkCaptivePortal(_ context.Context, conn *network.Connection, _ packet.Packet) bool { + if netenv.GetOnlineStatus() == netenv.StatusPortal && + conn.Entity.Domain == netenv.GetCaptivePortal().Domain { + conn.Accept("captive portal access permitted") + return true + } + + return false +} + func checkConnectionScope(_ context.Context, conn *network.Connection, _ packet.Packet) bool { p := conn.Process().Profile() diff --git a/nameserver/nameserver.go b/nameserver/nameserver.go index bc0a9735..f4d40155 100644 --- a/nameserver/nameserver.go +++ b/nameserver/nameserver.go @@ -115,7 +115,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er // return with server failure if offline if netenv.GetOnlineStatus() == netenv.StatusOffline && - !netenv.IsOnlineStatusTestDomain(q.FQDN) { + !netenv.IsConnectivityDomain(q.FQDN) { log.Tracer(ctx).Debugf("resolver: not resolving %s, device is offline", q.FQDN) returnServerFailure(w, query) return nil diff --git a/netenv/environment_darwin.go b/netenv/environment_darwin.go index a6eb794b..b9f7c0da 100644 --- a/netenv/environment_darwin.go +++ b/netenv/environment_darwin.go @@ -6,7 +6,7 @@ func Nameservers() []Nameserver { return nil } -func Gateways() []*net.IP { +func Gateways() []net.IP { return nil } diff --git a/netenv/environment_linux.go b/netenv/environment_linux.go index f704e8d5..952fb143 100644 --- a/netenv/environment_linux.go +++ b/netenv/environment_linux.go @@ -21,7 +21,7 @@ const ( ) var ( - gateways = make([]*net.IP, 0) + gateways = make([]net.IP, 0) gatewaysLock sync.Mutex gatewaysExpires = time.Now() @@ -31,7 +31,7 @@ var ( ) // Gateways returns the currently active gateways. -func Gateways() []*net.IP { +func Gateways() []net.IP { // locking gatewaysLock.Lock() defer gatewaysLock.Unlock() @@ -45,7 +45,7 @@ func Gateways() []*net.IP { }() // logic - newGateways := make([]*net.IP, 0) + newGateways := make([]net.IP, 0) var decoded []byte // open file @@ -77,7 +77,7 @@ func Gateways() []*net.IP { continue } gate := net.IPv4(decoded[3], decoded[2], decoded[1], decoded[0]) - newGateways = append(newGateways, &gate) + newGateways = append(newGateways, gate) } } @@ -110,7 +110,7 @@ func Gateways() []*net.IP { continue } gate := net.IP(decoded) - newGateways = append(newGateways, &gate) + newGateways = append(newGateways, gate) } } diff --git a/netenv/environment_windows.go b/netenv/environment_windows.go index c2bc4e81..fc1d5c3c 100644 --- a/netenv/environment_windows.go +++ b/netenv/environment_windows.go @@ -1,13 +1,95 @@ package netenv -import "net" +import ( + "bufio" + "bytes" + "context" + "net" + "os/exec" + "strings" + "sync" + "time" + + "github.com/safing/portbase/log" +) + +const ( + nameserversRecheck = 2 * time.Second +) + +var ( + nameservers = make([]Nameserver, 0) + nameserversLock sync.Mutex + nameserversExpires = time.Now() +) // Nameservers returns the currently active nameservers. func Nameservers() []Nameserver { - return nil + // locking + nameserversLock.Lock() + defer nameserversLock.Unlock() + // cache + if nameserversExpires.After(time.Now()) { + return nameservers + } + // update cache expiry when finished + defer func() { + nameserversExpires = time.Now().Add(nameserversRecheck) + }() + + // reset + nameservers = make([]Nameserver, 0) + + // This is a preliminary workaround until we have more time for proper interface using iphlpapi.dll + // TODO: make nice implementation + + var output = make(chan []byte, 1) + module.StartWorker("get assigned nameservers", func(ctx context.Context) error { + cmd := exec.CommandContext(ctx, "nslookup", "localhost") + data, err := cmd.CombinedOutput() + if err != nil { + log.Debugf("netenv: failed to get assigned nameserves: %s", err) + output <- nil + } else { + output <- data + } + return nil + }) + + select { + case data := <-output: + scanner := bufio.NewScanner(bytes.NewReader(data)) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + // check if we found the correct line + if !strings.HasPrefix(scanner.Text(), "Address:") { + continue + } + // split into fields, check if we have enough fields + fields := strings.Fields(scanner.Text()) + if len(fields) < 2 { + continue + } + // parse nameserver, return if valid IP found + ns := net.ParseIP(fields[1]) + if ns != nil { + nameservers = append(nameservers, Nameserver{ + IP: ns, + }) + return nameservers + } + } + log.Debug("netenv: could not find assigned nameserver") + return nameservers + + case <-time.After(5 * time.Second): + log.Debug("netenv: timed out while getting assigned nameserves") + } + + return nameservers } // Gateways returns the currently active gateways. -func Gateways() []*net.IP { +func Gateways() []net.IP { return nil } diff --git a/netenv/online-status.go b/netenv/online-status.go index 014efc8f..25574c9f 100644 --- a/netenv/online-status.go +++ b/netenv/online-status.go @@ -2,6 +2,7 @@ package netenv import ( "context" + "errors" "io/ioutil" "net" "net/http" @@ -41,6 +42,8 @@ const ( ResolverTestFqdn = "one.one.one.one." ResolverTestRRType = dns.TypeA ResolverTestExpectedResponse = "1.1.1.1" + + SpecialCaptivePortalDomain = "captiveportal.local." ) var ( @@ -62,12 +65,12 @@ func init() { } } -// IsOnlineStatusTestDomain checks whether the given fqdn is used for testing online status. -func IsOnlineStatusTestDomain(domain string) bool { +// IsConnectivityDomain checks whether the given domain (fqdn) is used for any connectivity related network connections and should always be resolved using the network assigned DNS server. +func IsConnectivityDomain(domain string) bool { switch domain { - case "detectportal.firefox.com.": - return true - case "one.one.one.one.": + case "detectportal.firefox.com.", + "one.one.one.one.", + GetCaptivePortal().Domain: return true } @@ -104,10 +107,34 @@ var ( onlineStatusInvestigationInProgress = abool.NewBool(false) onlineStatusInvestigationWg sync.WaitGroup - captivePortalURL string + captivePortal = &CaptivePortal{} captivePortalLock sync.Mutex ) +type CaptivePortal struct { + URL string + Domain string + IP net.IP +} + +// IPasRR returns the captive portal IP as a DNS resource record. +func (p *CaptivePortal) IPasRR() (rr dns.RR, err error) { + switch { + case p.IP == nil: + return nil, errors.New("no portal IP present") + case p.Domain == "": + return nil, errors.New("no portal domain present") + case p.IP.To4() != nil: + rr, err = dns.NewRR(p.Domain + " 17 IN A " + p.IP.String()) + default: + rr, err = dns.NewRR(p.Domain + " 17 IN AAAA " + p.IP.String()) + } + if err != nil { + return nil, err + } + return rr, nil +} + func init() { var onlineStatusValue int32 onlineStatus = &onlineStatusValue @@ -147,18 +174,16 @@ func updateOnlineStatus(status OnlineStatus, portalURL, comment string) { } // captive portal - captivePortalLock.Lock() - defer captivePortalLock.Unlock() - if portalURL != captivePortalURL { - captivePortalURL = portalURL - changed = true + // delete if offline, update only if there is a new value + if status == StatusOffline || portalURL != "" { + setCaptivePortal(portalURL) } // trigger event if changed { module.TriggerEvent(OnlineStatusChangedEvent, nil) if status == StatusPortal { - log.Infof(`network: setting online status to %s at "%s" (%s)`, status, captivePortalURL, comment) + log.Infof(`network: setting online status to %s at "%s" (%s)`, status, portalURL, comment) } else { log.Infof("network: setting online status to %s (%s)", status, comment) } @@ -166,12 +191,56 @@ func updateOnlineStatus(status OnlineStatus, portalURL, comment string) { } } -// GetCaptivePortalURL returns the current captive portal url as a string. -func GetCaptivePortalURL() string { +func setCaptivePortal(portalURL string) { captivePortalLock.Lock() defer captivePortalLock.Unlock() - return captivePortalURL + // delete + if portalURL == "" { + captivePortal = &CaptivePortal{} + return + } + + // set + captivePortal = &CaptivePortal{ + URL: portalURL, + } + parsedURL, err := url.Parse(portalURL) + switch { + case err != nil: + log.Debugf(`netenv: failed to parse captive portal URL "%s": %s`, portalURL, err) + return + case parsedURL.Hostname() == "": + log.Debugf(`netenv: captive portal URL "%s" has no domain or IP`, portalURL) + return + default: + // try to parse an IP + portalIP := net.ParseIP(parsedURL.Hostname()) + if portalIP != nil { + captivePortal.IP = portalIP + captivePortal.Domain = SpecialCaptivePortalDomain + return + } + + // try to parse domain + // ensure fqdn format + domain := dns.Fqdn(parsedURL.Hostname()) + // check validity + if !netutils.IsValidFqdn(domain) { + log.Debugf(`netenv: captive portal domain/IP "%s" is invalid`, parsedURL.Hostname()) + return + } + // set domain + captivePortal.Domain = domain + } +} + +// GetCaptivePortal returns the current captive portal. The returned struct must not be edited. +func GetCaptivePortal() *CaptivePortal { + captivePortalLock.Lock() + defer captivePortalLock.Unlock() + + return captivePortal } // ReportSuccessfulConnection hints the online status monitoring system that a connection attempt was successful. diff --git a/netenv/online-status_test.go b/netenv/online-status_test.go index 8c860720..7c61d9ab 100644 --- a/netenv/online-status_test.go +++ b/netenv/online-status_test.go @@ -8,5 +8,5 @@ import ( func TestCheckOnlineStatus(t *testing.T) { checkOnlineStatus(context.Background()) t.Logf("online status: %s", GetOnlineStatus()) - t.Logf("captive portal: %s", GetCaptivePortalURL()) + t.Logf("captive portal: %+v", GetCaptivePortal()) } diff --git a/network/connection.go b/network/connection.go index 4c5acbcf..7cd53f82 100644 --- a/network/connection.go +++ b/network/connection.go @@ -8,6 +8,8 @@ import ( "sync" "time" + "github.com/safing/portmaster/netenv" + "github.com/safing/portbase/database/record" "github.com/safing/portbase/log" "github.com/safing/portmaster/intel" @@ -148,6 +150,13 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection { } } + // check if destination IP is the captive portal's IP + portal := netenv.GetCaptivePortal() + if portal.IP != nil && pkt.Info().Dst.Equal(portal.IP) { + scope = portal.Domain + entity.Domain = portal.Domain + } + if scope == "" { // outbound direct (possibly P2P) connection diff --git a/resolver/resolve.go b/resolver/resolve.go index f6f62ca2..8b9c1927 100644 --- a/resolver/resolve.go +++ b/resolver/resolve.go @@ -19,7 +19,7 @@ var ( // basic errors // ErrNotFound is a basic error that will match all "not found" errors - ErrNotFound = errors.New("record does not exist") + ErrNotFound = errors.New("record could not be found") // ErrBlocked is basic error that will match all "blocked" errors ErrBlocked = errors.New("query was blocked") // ErrLocalhost is returned to *.localhost queries @@ -30,6 +30,8 @@ var ( ErrOffline = errors.New("device is offine") // ErrFailure is returned when the type of failure is unclear ErrFailure = errors.New("query failed") + // ErrContinue is returned when the resolver has no answer, and the next resolver should be asked + ErrContinue = errors.New("resolver has no answer") // detailed errors @@ -228,7 +230,7 @@ func resolveAndCache(ctx context.Context, q *Query) (rrCache *RRCache, err error // check if we are online if netenv.GetOnlineStatus() == netenv.StatusOffline { - if !netenv.IsOnlineStatusTestDomain(q.FQDN) { + if !netenv.IsConnectivityDomain(q.FQDN) { log.Tracer(ctx).Debugf("resolver: not resolving %s, device is offline", q.FQDN) // we are offline and this is not an online check query return nil, ErrOffline @@ -260,7 +262,7 @@ resolveLoop: // some resolvers might also block return nil, err case netenv.GetOnlineStatus() == netenv.StatusOffline && - !netenv.IsOnlineStatusTestDomain(q.FQDN): + !netenv.IsConnectivityDomain(q.FQDN): log.Tracer(ctx).Debugf("resolver: not resolving %s, device is offline", q.FQDN) // we are offline and this is not an online check query return nil, ErrOffline diff --git a/resolver/resolver-env.go b/resolver/resolver-env.go new file mode 100644 index 00000000..fd3ae961 --- /dev/null +++ b/resolver/resolver-env.go @@ -0,0 +1,95 @@ +package resolver + +import ( + "context" + "fmt" + "net" + + "github.com/miekg/dns" + "github.com/safing/portbase/log" + "github.com/safing/portmaster/netenv" + "github.com/safing/portmaster/network/netutils" +) + +var ( + envResolver = &Resolver{ + Server: ServerSourceEnv, + ServerType: ServerTypeEnv, + ServerIPScope: netutils.SiteLocal, + Source: ServerSourceEnv, + Conn: &envResolverConn{}, + } +) + +type envResolverConn struct{} + +func (er *envResolverConn) Query(ctx context.Context, q *Query) (*RRCache, error) { + // prepping + portal := netenv.GetCaptivePortal() + + // check for matching name + switch q.FQDN { + case netenv.SpecialCaptivePortalDomain: + if portal.IP != nil { + rr, err := portal.IPasRR() + if err != nil { + log.Warningf("nameserver: failed to create captive portal response to %s: %s", q.FQDN, err) + return nil, ErrNotFound + } + return er.makeRRCache(q, []dns.RR{rr}), nil + } + return nil, ErrNotFound + + case "router.local.": + routers := netenv.Gateways() + if len(routers) == 0 { + return nil, ErrNotFound + } + records, err := ipsToRRs(q.FQDN, routers) + if err != nil { + log.Warningf("nameserver: failed to create gateway response to %s: %s", q.FQDN, err) + return nil, ErrNotFound + } + return er.makeRRCache(q, records), nil + } + + // no match + return nil, ErrContinue // continue with next resolver +} + +func (er *envResolverConn) makeRRCache(q *Query, answers []dns.RR) *RRCache { + q.NoCaching = true // disable caching, as the env always has the data available and more up to date. + return &RRCache{ + Domain: q.FQDN, + Question: q.QType, + Answer: answers, + Server: envResolver.Server, + ServerScope: envResolver.ServerIPScope, + } +} + +func (er *envResolverConn) ReportFailure() {} + +func (er *envResolverConn) IsFailing() bool { + return false +} + +func ipsToRRs(domain string, ips []net.IP) ([]dns.RR, error) { + var records []dns.RR + var rr dns.RR + var err error + + for _, ip := range ips { + if ip.To4() != nil { + rr, err = dns.NewRR(domain + " 17 IN A " + ip.String()) + } else { + rr, err = dns.NewRR(domain + " 17 IN AAAA " + ip.String()) + } + if err != nil { + return nil, fmt.Errorf("failed to create record for %s: %w", ip, err) + } + records = append(records, rr) + } + + return records, nil +} diff --git a/resolver/resolver.go b/resolver/resolver.go index 19afb554..f89b051e 100644 --- a/resolver/resolver.go +++ b/resolver/resolver.go @@ -18,10 +18,12 @@ const ( ServerTypeTCP = "tcp" ServerTypeDoT = "dot" ServerTypeDoH = "doh" + ServerTypeEnv = "env" ServerSourceConfigured = "config" ServerSourceAssigned = "dhcp" ServerSourceMDNS = "mdns" + ServerSourceEnv = "env" ) var ( diff --git a/resolver/resolvers.go b/resolver/resolvers.go index 5b657378..0dfa3881 100644 --- a/resolver/resolvers.go +++ b/resolver/resolvers.go @@ -24,6 +24,7 @@ type Scope struct { var ( globalResolvers []*Resolver // all (global) resolvers localResolvers []*Resolver // all resolvers that are in site-local or link-local IP ranges + systemResolvers []*Resolver // all resolvers that were assigned by the system localScopes []*Scope // list of scopes with a list of local resolvers that can resolve the scope activeResolvers map[string]*Resolver // lookup map of all resolvers resolversLock sync.RWMutex @@ -231,7 +232,7 @@ func loadResolvers() { globalResolvers = newResolvers // assing resolvers to scopes - setLocalAndScopeResolvers(globalResolvers) + setScopedResolvers(globalResolvers) // set active resolvers (for cache validation) // reset @@ -241,6 +242,7 @@ func loadResolvers() { activeResolvers[resolver.Server] = resolver } activeResolvers[mDNSResolver.Server] = mDNSResolver + activeResolvers[envResolver.Server] = envResolver // log global resolvers if len(globalResolvers) > 0 { @@ -282,9 +284,10 @@ func loadResolvers() { } } -func setLocalAndScopeResolvers(resolvers []*Resolver) { +func setScopedResolvers(resolvers []*Resolver) { // make list with local resolvers localResolvers = make([]*Resolver, 0) + systemResolvers = make([]*Resolver, 0) localScopes = make([]*Scope, 0) for _, resolver := range resolvers { @@ -292,6 +295,10 @@ func setLocalAndScopeResolvers(resolvers []*Resolver) { localResolvers = append(localResolvers, resolver) } + if resolver.Source == "dhcp" { + systemResolvers = append(systemResolvers, resolver) + } + if resolver.Search != nil { // add resolver to custom searches for _, search := range resolver.Search { diff --git a/resolver/scopes.go b/resolver/scopes.go index 1b4946cf..2639eed4 100644 --- a/resolver/scopes.go +++ b/resolver/scopes.go @@ -5,6 +5,8 @@ import ( "errors" "strings" + "github.com/safing/portmaster/netenv" + "github.com/miekg/dns" "github.com/safing/portbase/log" ) @@ -124,6 +126,13 @@ func GetResolversInScope(ctx context.Context, q *Query) (selected []*Resolver) { // global -> local scopes, global // special -> local scopes, local + // special connectivity domains + if netenv.IsConnectivityDomain(q.FQDN) && len(systemResolvers) > 0 { + selected = append(selected, envResolver) + selected = append(selected, systemResolvers...) // dhcp assigned resolvers + return selected + } + // check local scopes for _, scope := range localScopes { if strings.HasSuffix(q.dotPrefixedFQDN, scope.Domain) { @@ -169,6 +178,8 @@ func GetResolversInScope(ctx context.Context, q *Query) (selected []*Resolver) { // check for .local mdns if strings.HasSuffix(q.dotPrefixedFQDN, local) { + // add env resolver + selected = append(selected, envResolver) // add mdns if err := mDNSResolver.checkCompliance(ctx, q); err == nil { selected = append(selected, mDNSResolver) @@ -255,6 +266,8 @@ func (resolver *Resolver) checkCompliance(_ context.Context, q *Query) error { // compliant case ServerTypeDoH: // compliant + case ServerTypeEnv: + // compliant (data is sources from local network only and is highly limited) default: return errInsecureProtocol } From 047352f9b53e695effe782b51564b018d4faa92b Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 15 Jul 2020 13:59:56 +0200 Subject: [PATCH 2/2] Implement review suggestions Fix local. resolving in Firefox --- netenv/environment_linux.go | 12 +++--- netenv/online-status.go | 20 +-------- network/connection.go | 2 +- network/netutils/{cleandns.go => dns.go} | 23 ++++++++++ .../{cleandns_test.go => dns_test.go} | 0 resolver/main.go | 4 ++ resolver/resolve.go | 2 + resolver/resolver-env.go | 42 ++++++++----------- resolver/reverse_test.go | 4 +- 9 files changed, 57 insertions(+), 52 deletions(-) rename network/netutils/{cleandns.go => dns.go} (73%) rename network/netutils/{cleandns_test.go => dns_test.go} (100%) diff --git a/netenv/environment_linux.go b/netenv/environment_linux.go index 952fb143..31297de1 100644 --- a/netenv/environment_linux.go +++ b/netenv/environment_linux.go @@ -45,14 +45,14 @@ func Gateways() []net.IP { }() // logic - newGateways := make([]net.IP, 0) + gateways = make([]net.IP, 0) var decoded []byte // open file route, err := os.Open("/proc/net/route") if err != nil { log.Warningf("environment: could not read /proc/net/route: %s", err) - return newGateways + return gateways } defer route.Close() @@ -77,7 +77,7 @@ func Gateways() []net.IP { continue } gate := net.IPv4(decoded[3], decoded[2], decoded[1], decoded[0]) - newGateways = append(newGateways, gate) + gateways = append(gateways, gate) } } @@ -85,7 +85,7 @@ func Gateways() []net.IP { v6route, err := os.Open("/proc/net/ipv6_route") if err != nil { log.Warningf("environment: could not read /proc/net/ipv6_route: %s", err) - return newGateways + return gateways } defer v6route.Close() @@ -110,11 +110,11 @@ func Gateways() []net.IP { continue } gate := net.IP(decoded) - newGateways = append(newGateways, gate) + gateways = append(gateways, gate) } } - return newGateways + return gateways } // Nameservers returns the currently active nameservers. diff --git a/netenv/online-status.go b/netenv/online-status.go index 25574c9f..c56e975d 100644 --- a/netenv/online-status.go +++ b/netenv/online-status.go @@ -2,7 +2,6 @@ package netenv import ( "context" - "errors" "io/ioutil" "net" "net/http" @@ -111,30 +110,13 @@ var ( captivePortalLock sync.Mutex ) +// CaptivePortal holds information about a detected captive portal. type CaptivePortal struct { URL string Domain string IP net.IP } -// IPasRR returns the captive portal IP as a DNS resource record. -func (p *CaptivePortal) IPasRR() (rr dns.RR, err error) { - switch { - case p.IP == nil: - return nil, errors.New("no portal IP present") - case p.Domain == "": - return nil, errors.New("no portal domain present") - case p.IP.To4() != nil: - rr, err = dns.NewRR(p.Domain + " 17 IN A " + p.IP.String()) - default: - rr, err = dns.NewRR(p.Domain + " 17 IN AAAA " + p.IP.String()) - } - if err != nil { - return nil, err - } - return rr, nil -} - func init() { var onlineStatusValue int32 onlineStatus = &onlineStatusValue diff --git a/network/connection.go b/network/connection.go index 7cd53f82..d05ef682 100644 --- a/network/connection.go +++ b/network/connection.go @@ -152,7 +152,7 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection { // check if destination IP is the captive portal's IP portal := netenv.GetCaptivePortal() - if portal.IP != nil && pkt.Info().Dst.Equal(portal.IP) { + if pkt.Info().Dst.Equal(portal.IP) { scope = portal.Domain entity.Domain = portal.Domain } diff --git a/network/netutils/cleandns.go b/network/netutils/dns.go similarity index 73% rename from network/netutils/cleandns.go rename to network/netutils/dns.go index 7df738d0..68bb5d54 100644 --- a/network/netutils/cleandns.go +++ b/network/netutils/dns.go @@ -1,6 +1,8 @@ package netutils import ( + "fmt" + "net" "regexp" "github.com/miekg/dns" @@ -56,3 +58,24 @@ func IsValidFqdn(fqdn string) bool { _, ok := dns.IsDomainName(fqdn) return ok } + +// IPsToRRs transforms the given IPs to resource records. +func IPsToRRs(domain string, ips []net.IP) ([]dns.RR, error) { + records := make([]dns.RR, 0, len(ips)) + var rr dns.RR + var err error + + for _, ip := range ips { + if ip.To4() != nil { + rr, err = dns.NewRR(fmt.Sprintf("%s 17 IN A %s", domain, ip)) + } else { + rr, err = dns.NewRR(fmt.Sprintf("%s 17 IN AAAA %s", domain, ip)) + } + if err != nil { + return nil, fmt.Errorf("failed to create record for %s: %w", ip, err) + } + records = append(records, rr) + } + + return records, nil +} diff --git a/network/netutils/cleandns_test.go b/network/netutils/dns_test.go similarity index 100% rename from network/netutils/cleandns_test.go rename to network/netutils/dns_test.go diff --git a/resolver/main.go b/resolver/main.go index 05d20fe3..57370c55 100644 --- a/resolver/main.go +++ b/resolver/main.go @@ -24,6 +24,10 @@ func init() { func prep() error { intel.SetReverseResolver(ResolveIPAndValidate) + if err := prepEnvResolver(); err != nil { + return err + } + return prepConfig() } diff --git a/resolver/resolve.go b/resolver/resolve.go index 8b9c1927..e3a24dcf 100644 --- a/resolver/resolve.go +++ b/resolver/resolve.go @@ -261,6 +261,8 @@ resolveLoop: case errors.Is(err, ErrBlocked): // some resolvers might also block return nil, err + case errors.Is(err, ErrContinue): + continue case netenv.GetOnlineStatus() == netenv.StatusOffline && !netenv.IsConnectivityDomain(q.FQDN): log.Tracer(ctx).Debugf("resolver: not resolving %s, device is offline", q.FQDN) diff --git a/resolver/resolver-env.go b/resolver/resolver-env.go index fd3ae961..6b987b28 100644 --- a/resolver/resolver-env.go +++ b/resolver/resolver-env.go @@ -2,7 +2,6 @@ package resolver import ( "context" - "fmt" "net" "github.com/miekg/dns" @@ -19,8 +18,15 @@ var ( Source: ServerSourceEnv, Conn: &envResolverConn{}, } + + localSOA dns.RR ) +func prepEnvResolver() (err error) { + localSOA, err = dns.NewRR("local. 17 IN SOA localhost. none.localhost. 17 17 17 17 17") + return err +} + type envResolverConn struct{} func (er *envResolverConn) Query(ctx context.Context, q *Query) (*RRCache, error) { @@ -29,14 +35,22 @@ func (er *envResolverConn) Query(ctx context.Context, q *Query) (*RRCache, error // check for matching name switch q.FQDN { + case "local.": + // Firefox requests the SOA request for local. before resolving any local. domains. + // Others might be doing this too. We guessed this behaviour, weren't able to find docs. + if q.QType == dns.Type(dns.TypeSOA) { + return er.makeRRCache(q, []dns.RR{localSOA}), nil + } + return nil, ErrNotFound + case netenv.SpecialCaptivePortalDomain: if portal.IP != nil { - rr, err := portal.IPasRR() + records, err := netutils.IPsToRRs(q.FQDN, []net.IP{portal.IP}) if err != nil { log.Warningf("nameserver: failed to create captive portal response to %s: %s", q.FQDN, err) return nil, ErrNotFound } - return er.makeRRCache(q, []dns.RR{rr}), nil + return er.makeRRCache(q, records), nil } return nil, ErrNotFound @@ -45,7 +59,7 @@ func (er *envResolverConn) Query(ctx context.Context, q *Query) (*RRCache, error if len(routers) == 0 { return nil, ErrNotFound } - records, err := ipsToRRs(q.FQDN, routers) + records, err := netutils.IPsToRRs(q.FQDN, routers) if err != nil { log.Warningf("nameserver: failed to create gateway response to %s: %s", q.FQDN, err) return nil, ErrNotFound @@ -73,23 +87,3 @@ func (er *envResolverConn) ReportFailure() {} func (er *envResolverConn) IsFailing() bool { return false } - -func ipsToRRs(domain string, ips []net.IP) ([]dns.RR, error) { - var records []dns.RR - var rr dns.RR - var err error - - for _, ip := range ips { - if ip.To4() != nil { - rr, err = dns.NewRR(domain + " 17 IN A " + ip.String()) - } else { - rr, err = dns.NewRR(domain + " 17 IN AAAA " + ip.String()) - } - if err != nil { - return nil, fmt.Errorf("failed to create record for %s: %w", ip, err) - } - records = append(records, rr) - } - - return records, nil -} diff --git a/resolver/reverse_test.go b/resolver/reverse_test.go index a16dcc0a..e162e823 100644 --- a/resolver/reverse_test.go +++ b/resolver/reverse_test.go @@ -32,6 +32,6 @@ func TestResolveIPAndValidate(t *testing.T) { testReverse(t, "1.1.1.1", "one.one.one.one.", "") testReverse(t, "2606:4700:4700::1111", "one.one.one.one.", "") - testReverse(t, "93.184.216.34", "example.com.", "record does not exist: 34.216.184.93.in-addr.arpa.PTR") - testReverse(t, "185.199.109.153", "sites.github.io.", "record does not exist: 153.109.199.185.in-addr.arpa.PTR") + testReverse(t, "93.184.216.34", "example.com.", "record could not be found: 34.216.184.93.in-addr.arpa.PTR") + testReverse(t, "185.199.109.153", "sites.github.io.", "record could not be found: 153.109.199.185.in-addr.arpa.PTR") }