From b9b33ed2b343f69ff02d11907b26f822749c1f31 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 19 Oct 2021 10:25:49 +0200 Subject: [PATCH 1/3] Use separate DNSRequestContext struct for adding DNS context to connections --- firewall/dns.go | 8 ++++---- nameserver/nameserver.go | 24 +++++++++++------------- network/connection.go | 6 +++--- resolver/ipinfo.go | 4 ++-- resolver/rr_context.go | 39 +++++++++++++++++++++++++++++++++++++++ resolver/rrcache.go | 20 -------------------- 6 files changed, 59 insertions(+), 42 deletions(-) create mode 100644 resolver/rr_context.go diff --git a/firewall/dns.go b/firewall/dns.go index 02268eb3..5aaeff26 100644 --- a/firewall/dns.go +++ b/firewall/dns.go @@ -263,10 +263,10 @@ func UpdateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *netw // Create new record for this IP. record := resolver.ResolvedDomain{ - Domain: q.FQDN, - RRCache: rrCache, - Resolver: rrCache.Resolver, - Expires: rrCache.Expires, + Domain: q.FQDN, + Resolver: rrCache.Resolver, + DNSRequestContext: rrCache.ToDNSRequestContext(), + Expires: rrCache.Expires, } // Resolve all CNAMEs in the correct order and add the to the record. diff --git a/nameserver/nameserver.go b/nameserver/nameserver.go index 3efb5635..44a37b2f 100644 --- a/nameserver/nameserver.go +++ b/nameserver/nameserver.go @@ -141,6 +141,12 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg) // Once we decided on the connection we might need to save it to the database, // so we defer that check for now. defer func() { + // Add metadata to connection. + if rrCache != nil { + conn.DNSContext = rrCache.ToDNSRequestContext() + conn.Resolver = rrCache.Resolver + } + switch conn.Verdict { // We immediately save blocked, dropped or failed verdicts so // they pop up in the UI. @@ -222,27 +228,19 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg) } } // Handle special cases. - if rrCache == nil { + switch { + case rrCache == nil: tracer.Warning("nameserver: received successful, but empty reply from resolver") return reply(nsutil.ServerFailure("internal error: empty reply")) - } - - // Add dns context and resolver to connection. - conn.DNSContext = rrCache - conn.Resolver = rrCache.Resolver - - // Return now if NXDomain. - if rrCache.RCode == dns.RcodeNameError { + case rrCache.RCode == dns.RcodeNameError: + // Return now if NXDomain. return reply(nsutil.NxDomain("no answer found (NXDomain)")) } + // Check with firewall again after resolving. tracer.Trace("nameserver: deciding on resolved dns") rrCache = firewall.FilterResolvedDNS(ctx, conn, q, rrCache) - // Add dns context and resolver to connection. - conn.DNSContext = rrCache - conn.Resolver = rrCache.Resolver - // Check again if there is a responder from the firewall. if responder, ok := conn.Reason.Context.(nsutil.Responder); ok { tracer.Infof("nameserver: handing over request for %s to special filter responder: %s", q.ID(), conn.Reason.Msg) diff --git a/network/connection.go b/network/connection.go index 94ab3c31..37005665 100644 --- a/network/connection.go +++ b/network/connection.go @@ -145,7 +145,7 @@ type Connection struct { //nolint:maligned // TODO: fix alignment ProcessContext ProcessContext // DNSContext holds additional information about the DNS request that was // probably used to resolve the IP of this connection. - DNSContext *resolver.RRCache + DNSContext *resolver.DNSRequestContext // TunnelContext holds additional information about the tunnel that this // connection is using. TunnelContext interface{} @@ -333,7 +333,7 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection { var scope string var resolverInfo *resolver.ResolverInfo - var dnsContext *resolver.RRCache + var dnsContext *resolver.DNSRequestContext if inbound { @@ -365,7 +365,7 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection { scope = lastResolvedDomain.Domain entity.Domain = lastResolvedDomain.Domain entity.CNAME = lastResolvedDomain.CNAMEs - dnsContext = lastResolvedDomain.RRCache + dnsContext = lastResolvedDomain.DNSRequestContext resolverInfo = lastResolvedDomain.Resolver removeOpenDNSRequest(proc.Pid, lastResolvedDomain.Domain) } diff --git a/resolver/ipinfo.go b/resolver/ipinfo.go index 50a2eb75..ff48dbde 100644 --- a/resolver/ipinfo.go +++ b/resolver/ipinfo.go @@ -44,8 +44,8 @@ type ResolvedDomain struct { // information. Resolver *ResolverInfo - // RRCache holds the DNS response that was received for this domain. - RRCache *RRCache + // DNSRequestContext holds the DNS request context. + DNSRequestContext *DNSRequestContext // Expires holds the timestamp when this entry expires. // This does not mean that the entry may not be used anymore afterwards, diff --git a/resolver/rr_context.go b/resolver/rr_context.go new file mode 100644 index 00000000..98fb7802 --- /dev/null +++ b/resolver/rr_context.go @@ -0,0 +1,39 @@ +package resolver + +import ( + "time" + + "github.com/miekg/dns" +) + +// DNSRequestContext is a static structure to add information to DNS request connections. +type DNSRequestContext struct { + Domain string + Question string + RCode string + + ServedFromCache bool + RequestingNew bool + IsBackup bool + Filtered bool + + Modified time.Time + Expires time.Time +} + +// ToDNSRequestContext returns a new DNSRequestContext of the RRCache. +func (rrCache *RRCache) ToDNSRequestContext() *DNSRequestContext { + return &DNSRequestContext{ + Domain: rrCache.Domain, + Question: rrCache.Question.String(), + RCode: dns.RcodeToString[rrCache.RCode], + + ServedFromCache: rrCache.ServedFromCache, + RequestingNew: rrCache.RequestingNew, + IsBackup: rrCache.IsBackup, + Filtered: rrCache.Filtered, + + Modified: time.Unix(rrCache.Modified, 0), + Expires: time.Unix(rrCache.Expires, 0), + } +} diff --git a/resolver/rrcache.go b/resolver/rrcache.go index 05a75ce1..de62b4e6 100644 --- a/resolver/rrcache.go +++ b/resolver/rrcache.go @@ -2,7 +2,6 @@ package resolver import ( "context" - "encoding/json" "fmt" "net" "time" @@ -45,25 +44,6 @@ type RRCache struct { Modified int64 } -func (rrCache *RRCache) MarshalJSON() ([]byte, error) { - var record = struct { - RRCache - - Question string - RCode string - Modified time.Time - Expires time.Time - }{ - RRCache: *rrCache, - Question: rrCache.Question.String(), - RCode: dns.RcodeToString[rrCache.RCode], - Modified: time.Unix(rrCache.Modified, 0), - Expires: time.Unix(rrCache.Expires, 0), - } - - return json.Marshal(record) -} - // ID returns the ID of the RRCache consisting of the domain and question type. func (rrCache *RRCache) ID() string { return rrCache.Domain + rrCache.Question.String() From 0addff944971b0f1c7dd92728cd4e707cccaf557 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 19 Oct 2021 10:26:28 +0200 Subject: [PATCH 2/3] Make online status checking SPN aware --- netenv/online-status.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/netenv/online-status.go b/netenv/online-status.go index c5983f7c..db36a805 100644 --- a/netenv/online-status.go +++ b/netenv/online-status.go @@ -42,6 +42,8 @@ var ( DNSFallbackTestDomain = "dns-check.safing.io." DNSFallbackTestExpectedIP = net.IPv4(0, 65, 67, 75) // Ascii: \0ACK + ConnectedToSPN = abool.New() + // SpecialCaptivePortalDomain is the domain name used to point to the detected captive portal IP // or the captive portal test IP. The default value should be overridden by the resolver package, // which defines the custom internal domain name to use. @@ -350,6 +352,12 @@ func checkOnlineStatus(ctx context.Context) { return StatusUnknown }*/ + // 0) check if connected to SPN + + if ConnectedToSPN.IsSet() { + updateOnlineStatus(StatusOnline, nil, "connected to SPN") + } + // 1) check for addresses ipv4, ipv6, err := GetAssignedAddresses() From 0d88a6982a8351305021eee9a254542b9c49cb9a Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 19 Oct 2021 10:49:19 +0200 Subject: [PATCH 3/3] Improve and document location estimation algorithm --- intel/geoip/location.go | 109 ++++++++++++++++++++++++---------------- 1 file changed, 66 insertions(+), 43 deletions(-) diff --git a/intel/geoip/location.go b/intel/geoip/location.go index 152004f0..f9f62f96 100644 --- a/intel/geoip/location.go +++ b/intel/geoip/location.go @@ -30,57 +30,55 @@ type Coordinates struct { Longitude float64 `maxminddb:"longitude"` } -// About GeoLite2 City accuracy_radius: -// -// range: 1-1000 -// seen values (from memory): 1,5,10,20,50,100,200,500,1000 -// default seems to be 100 -// -// examples: -// 1.1.1/24 has 1000: Anycast -// 8.8.0/19 has 1000: Anycast -// 8.8.52/22 has 1: City of Westfield -// -// Conclusion: -// - Ignore location data completely if accuracy_radius > 500 - // EstimateNetworkProximity aims to calculate the distance between two network locations. Returns a proximity value between 0 (far away) and 100 (nearby). func (l *Location) EstimateNetworkProximity(to *Location) (proximity int) { - // Distance Value: - // 0: other side of the Internet - // 100: same network/datacenter + /* + Distance Value - // Weighting: - // continent match: 25 - // country match: 20 - // AS owner match: 25 - // AS network match: 20 - // coordinate distance: 0-10 + - 0: Other side of the Internet. + - 100: Very near, up to same network / datacenter. - // continent match: 25 - if l.Continent.Code == to.Continent.Code { - proximity += 25 - // country match: 20 - if l.Country.ISOCode == to.Country.ISOCode { - proximity += 20 + Weighting Goal + + - Exposure to different networks shall be limited as much as possible. + - A single network should not see a connection over a large distance. + - Latency should be low. + + Weighting Intentions + + - Being on the same continent is better than being in the same AS. + - Being in the same country is better than having low coordinate distance. + - Coordinate distance is only a tie breaker, as accuracy varies heavily. + - Same AS with lower coordinate distance beats being on the same continent. + + Weighting Configuration + + - Continent match: 30 + - Country match: 25 + - ASOrg match: 20 + - ASN match: 15 + - Coordinate distance: 0-10 + */ + + if l.Continent.Code != "" && + l.Continent.Code == to.Continent.Code { + proximity += 30 + if l.Country.ISOCode != "" && + l.Country.ISOCode == to.Country.ISOCode { + proximity += 25 } } - // AS owner match: 25 - if l.AutonomousSystemOrganization == to.AutonomousSystemOrganization { - proximity += 25 - // AS network match: 20 - if l.AutonomousSystemNumber == to.AutonomousSystemNumber { - proximity += 20 + if l.AutonomousSystemOrganization != "" && + l.AutonomousSystemOrganization == to.AutonomousSystemOrganization { + proximity += 20 + if l.AutonomousSystemNumber != 0 && + l.AutonomousSystemNumber == to.AutonomousSystemNumber { + proximity += 15 } } - // coordinate distance: 0-10 - fromCoords := haversine.Coord{Lat: l.Coordinates.Latitude, Lon: l.Coordinates.Longitude} - toCoords := haversine.Coord{Lat: to.Coordinates.Latitude, Lon: to.Coordinates.Longitude} - _, km := haversine.Distance(fromCoords, toCoords) - - // adjust accuracy value + // Check coordinates and adjust accuracy value. accuracy := l.Coordinates.AccuracyRadius switch { case l.Coordinates.Latitude == 0 && l.Coordinates.Longitude == 0: @@ -93,12 +91,37 @@ func (l *Location) EstimateNetworkProximity(to *Location) (proximity int) { accuracy = to.Coordinates.AccuracyRadius } - if km <= 10 && accuracy <= 100 { + /* + About the Accuracy Radius + + - Range: 1-1000 + - Seen values (estimation): 1,5,10,20,50,100,200,500,1000 + - The default seems to be 100. + + Cxamples + + - 1.1.1/24 has 1000: Anycast + - 8.8.0/19 has 1000: Anycast + - 8.8.52/22 has 1: City of Westfield + + Conclusion + + - Ignore or penalize high accuracy radius. + */ + + // Calculate coordinate distance in kilometers. + fromCoords := haversine.Coord{Lat: l.Coordinates.Latitude, Lon: l.Coordinates.Longitude} + toCoords := haversine.Coord{Lat: to.Coordinates.Latitude, Lon: to.Coordinates.Longitude} + _, km := haversine.Distance(fromCoords, toCoords) + + if km <= 50 && accuracy <= 100 { + // Give a flat out ten for highly accurate coordinates within 50km. proximity += 10 } else { + // Else, take a percentage. distanceInPercent := (earthCircumferenceInKm - km) * 100 / earthCircumferenceInKm - // apply penalty for locations with low accuracy (targeting accuracy radius >100) + // Apply penalty for locations with low accuracy (targeting accuracy radius >100). accuracyModifier := 1 - float64(accuracy)/2000 proximity += int(distanceInPercent * 0.10 * accuracyModifier) }