Merge pull request #277 from safing/feature/system-resolver-compat

Add system resolver compatibility workaround
This commit is contained in:
Daniel 2021-03-23 14:42:00 +01:00 committed by GitHub
commit f41696cea7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 760 additions and 428 deletions

View file

@ -16,7 +16,7 @@ import (
"github.com/safing/portmaster/resolver" "github.com/safing/portmaster/resolver"
) )
func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, scope int8) ([]dns.RR, []string, int, string) { func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, resolverScope netutils.IPScope, sysResolver bool) ([]dns.RR, []string, int, string) {
goodEntries := make([]dns.RR, 0, len(entries)) goodEntries := make([]dns.RR, 0, len(entries))
filteredRecords := make([]string, 0, len(entries)) filteredRecords := make([]string, 0, len(entries))
@ -38,16 +38,16 @@ func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, scope int8) (
goodEntries = append(goodEntries, rr) goodEntries = append(goodEntries, rr)
continue continue
} }
classification := netutils.ClassifyIP(ip) ipScope := netutils.GetIPScope(ip)
if p.RemoveOutOfScopeDNS() { if p.RemoveOutOfScopeDNS() {
switch { switch {
case classification == netutils.HostLocal: case ipScope.IsLocalhost():
// No DNS should return localhost addresses // No DNS should return localhost addresses
filteredRecords = append(filteredRecords, rr.String()) filteredRecords = append(filteredRecords, rr.String())
interveningOptionKey = profile.CfgOptionRemoveOutOfScopeDNSKey interveningOptionKey = profile.CfgOptionRemoveOutOfScopeDNSKey
continue continue
case scope == netutils.Global && (classification == netutils.SiteLocal || classification == netutils.LinkLocal): case resolverScope.IsGlobal() && ipScope.IsLAN() && !sysResolver:
// No global DNS should return LAN addresses // No global DNS should return LAN addresses
filteredRecords = append(filteredRecords, rr.String()) filteredRecords = append(filteredRecords, rr.String())
interveningOptionKey = profile.CfgOptionRemoveOutOfScopeDNSKey interveningOptionKey = profile.CfgOptionRemoveOutOfScopeDNSKey
@ -55,18 +55,18 @@ func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, scope int8) (
} }
} }
if p.RemoveBlockedDNS() { if p.RemoveBlockedDNS() && !sysResolver {
// filter by flags // filter by flags
switch { switch {
case p.BlockScopeInternet() && classification == netutils.Global: case p.BlockScopeInternet() && ipScope.IsGlobal():
filteredRecords = append(filteredRecords, rr.String()) filteredRecords = append(filteredRecords, rr.String())
interveningOptionKey = profile.CfgOptionBlockScopeInternetKey interveningOptionKey = profile.CfgOptionBlockScopeInternetKey
continue continue
case p.BlockScopeLAN() && (classification == netutils.SiteLocal || classification == netutils.LinkLocal): case p.BlockScopeLAN() && ipScope.IsLAN():
filteredRecords = append(filteredRecords, rr.String()) filteredRecords = append(filteredRecords, rr.String())
interveningOptionKey = profile.CfgOptionBlockScopeLANKey interveningOptionKey = profile.CfgOptionBlockScopeLANKey
continue continue
case p.BlockScopeLocal() && classification == netutils.HostLocal: case p.BlockScopeLocal() && ipScope.IsLocalhost():
filteredRecords = append(filteredRecords, rr.String()) filteredRecords = append(filteredRecords, rr.String())
interveningOptionKey = profile.CfgOptionBlockScopeLocalKey interveningOptionKey = profile.CfgOptionBlockScopeLocalKey
continue continue
@ -83,7 +83,7 @@ func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, scope int8) (
return goodEntries, filteredRecords, allowedAddressRecords, interveningOptionKey return goodEntries, filteredRecords, allowedAddressRecords, interveningOptionKey
} }
func filterDNSResponse(conn *network.Connection, rrCache *resolver.RRCache) *resolver.RRCache { func filterDNSResponse(conn *network.Connection, rrCache *resolver.RRCache, sysResolver bool) *resolver.RRCache {
p := conn.Process().Profile() p := conn.Process().Profile()
// do not modify own queries // do not modify own queries
@ -104,11 +104,11 @@ func filterDNSResponse(conn *network.Connection, rrCache *resolver.RRCache) *res
var validIPs int var validIPs int
var interveningOptionKey string var interveningOptionKey string
rrCache.Answer, filteredRecords, validIPs, interveningOptionKey = filterDNSSection(rrCache.Answer, p, rrCache.ServerScope) rrCache.Answer, filteredRecords, validIPs, interveningOptionKey = filterDNSSection(rrCache.Answer, p, rrCache.Resolver.IPScope, sysResolver)
rrCache.FilteredEntries = append(rrCache.FilteredEntries, filteredRecords...) rrCache.FilteredEntries = append(rrCache.FilteredEntries, filteredRecords...)
// we don't count the valid IPs in the extra section // we don't count the valid IPs in the extra section
rrCache.Extra, filteredRecords, _, _ = filterDNSSection(rrCache.Extra, p, rrCache.ServerScope) rrCache.Extra, filteredRecords, _, _ = filterDNSSection(rrCache.Extra, p, rrCache.Resolver.IPScope, sysResolver)
rrCache.FilteredEntries = append(rrCache.FilteredEntries, filteredRecords...) rrCache.FilteredEntries = append(rrCache.FilteredEntries, filteredRecords...)
if len(rrCache.FilteredEntries) > 0 { if len(rrCache.FilteredEntries) > 0 {
@ -160,8 +160,9 @@ func filterDNSResponse(conn *network.Connection, rrCache *resolver.RRCache) *res
return rrCache return rrCache
} }
// DecideOnResolvedDNS filters a dns response according to the application profile and settings. // FilterResolvedDNS filters a dns response according to the application
func DecideOnResolvedDNS( // profile and settings.
func FilterResolvedDNS(
ctx context.Context, ctx context.Context,
conn *network.Connection, conn *network.Connection,
q *resolver.Query, q *resolver.Query,
@ -174,14 +175,15 @@ func DecideOnResolvedDNS(
return rrCache return rrCache
} }
updatedRR := filterDNSResponse(conn, rrCache) // Only filter criticial things if request comes from the system resolver.
sysResolver := conn.Process().IsSystemResolver()
updatedRR := filterDNSResponse(conn, rrCache, sysResolver)
if updatedRR == nil { if updatedRR == nil {
return nil return nil
} }
updateIPsAndCNAMEs(q, rrCache, conn) if !sysResolver && mayBlockCNAMEs(ctx, conn) {
if mayBlockCNAMEs(ctx, conn) {
return nil return nil
} }
@ -213,14 +215,23 @@ func mayBlockCNAMEs(ctx context.Context, conn *network.Connection) bool {
return false return false
} }
// updateIPsAndCNAMEs saves all the IP->Name mappings to the cache database and // UpdateIPsAndCNAMEs saves all the IP->Name mappings to the cache database and
// updates the CNAMEs in the Connection's Entity. // updates the CNAMEs in the Connection's Entity.
func updateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *network.Connection) { func UpdateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *network.Connection) {
// Sanity check input, as this is called from defer.
if q == nil || rrCache == nil {
return
}
// Get profileID for scoping IPInfo. // Get profileID for scoping IPInfo.
var profileID string var profileID string
proc := conn.Process() localProfile := conn.Process().Profile().LocalProfile()
if proc != nil { switch localProfile.ID {
profileID = proc.LocalProfileKey case profile.UnidentifiedProfileID,
profile.SystemResolverProfileID:
profileID = resolver.IPInfoProfileScopeGlobal
default:
profileID = localProfile.ID
} }
// Collect IPs and CNAMEs. // Collect IPs and CNAMEs.
@ -249,8 +260,9 @@ func updateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *netw
// Create new record for this IP. // Create new record for this IP.
record := resolver.ResolvedDomain{ record := resolver.ResolvedDomain{
Domain: q.FQDN, Domain: q.FQDN,
Expires: rrCache.Expires, Expires: rrCache.Expires,
Resolver: rrCache.Resolver,
} }
// Resolve all CNAMEs in the correct order and add the to the record. // Resolve all CNAMEs in the correct order and add the to the record.

View file

@ -286,7 +286,7 @@ func initialHandler(conn *network.Connection, pkt packet.Packet) {
// TODO: add implementation for forced tunneling // TODO: add implementation for forced tunneling
if pkt.IsOutbound() && if pkt.IsOutbound() &&
captain.ClientReady() && captain.ClientReady() &&
netutils.IPIsGlobal(conn.Entity.IP) && conn.Entity.IPScope.IsGlobal() &&
conn.Verdict == network.VerdictAccept { conn.Verdict == network.VerdictAccept {
// try to tunnel // try to tunnel
err := sluice.AwaitRequest(pkt.Info(), conn.Entity.Domain) err := sluice.AwaitRequest(pkt.Info(), conn.Entity.Domain)

View file

@ -9,7 +9,7 @@ import (
// start starts the interception. // start starts the interception.
func start(_ chan packet.Packet) error { func start(_ chan packet.Packet) error {
log.Info("interception: this platform has no support for packet interception - a lot of functionality will be broken") log.Critical("interception: this platform has no support for packet interception - a lot of functionality will be broken")
return nil return nil
} }

View file

@ -33,7 +33,7 @@ func start(ch chan packet.Packet) error {
} }
go windowskext.Handler(ch) go windowskext.Handler(ch)
go handleWindowsDNSCache() go checkWindowsDNSCache()
return nil return nil
} }
@ -43,37 +43,21 @@ func stop() error {
return windowskext.Stop() return windowskext.Stop()
} }
func handleWindowsDNSCache() { func checkWindowsDNSCache() {
status, err := osdetail.GetServiceStatus("dnscache")
err := osdetail.StopService("dnscache")
if err != nil { if err != nil {
// cannot stop dnscache, try disabling log.Warningf("firewall/interception: failed to check status of Windows DNS-Client: %s", err)
if err == osdetail.ErrServiceNotStoppable {
err := osdetail.DisableDNSCache()
if err != nil {
log.Warningf("firewall/interception: failed to disable Windows Service \"DNS Client\" (dnscache) for better interception: %s", err)
notifyDisableDNSCache()
}
notifyRebootRequired()
return
}
// error while stopping service
log.Warningf("firewall/interception: failed to stop Windows Service \"DNS Client\" (dnscache) for better interception: %s", err)
notifyDisableDNSCache()
} }
// log that service is stopped if status == osdetail.StatusStopped {
log.Info("firewall/interception: Windows Service \"DNS Client\" (dnscache) is stopped for better interception") err := osdetail.EnableDNSCache()
if err != nil {
} log.Warningf("firewall/interception: failed to enable Windows Service \"DNS Client\" (dnscache): %s", err)
} else {
func notifyDisableDNSCache() { log.Warningf("firewall/interception: successfully enabled the dnscache")
(&notifications.Notification{ notifyRebootRequired()
EventID: "interception:windows-disable-dns-cache", }
Message: "The Portmaster needs the Windows Service \"DNS Client\" (dnscache) to be disabled for best effectiveness.", }
Type: notifications.Warning,
}).Save()
} }
func notifyRebootRequired() { func notifyRebootRequired() {

View file

@ -39,7 +39,7 @@ const noReasonOptionKey = ""
type deciderFn func(context.Context, *network.Connection, packet.Packet) bool type deciderFn func(context.Context, *network.Connection, packet.Packet) bool
var deciders = []deciderFn{ var defaultDeciders = []deciderFn{
checkPortmasterConnection, checkPortmasterConnection,
checkSelfCommunication, checkSelfCommunication,
checkConnectionType, checkConnectionType,
@ -53,6 +53,11 @@ var deciders = []deciderFn{
checkAutoPermitRelated, checkAutoPermitRelated,
} }
var dnsFromSystemResolverDeciders = []deciderFn{
checkConnectivityDomain,
checkBypassPrevention,
}
// DecideOnConnection makes a decision about a connection. // DecideOnConnection makes a decision about a connection.
// When called, the connection and profile is already locked. // When called, the connection and profile is already locked.
func DecideOnConnection(ctx context.Context, conn *network.Connection, pkt packet.Packet) { func DecideOnConnection(ctx context.Context, conn *network.Connection, pkt packet.Packet) {
@ -79,8 +84,21 @@ func DecideOnConnection(ctx context.Context, conn *network.Connection, pkt packe
} }
} }
// DNS request from the system resolver require a special decision process,
// because the original requesting process is not known. Here, we only check
// global-only and the most important per-app aspects. The resulting
// connection is then blocked when the original requesting process is known.
if conn.Type == network.DNSRequest && conn.Process().IsSystemResolver() {
// Run all deciders and return if they came to a conclusion.
done, _ := runDeciders(ctx, dnsFromSystemResolverDeciders, conn, pkt)
if !done {
conn.Accept("permitting system resolver dns request", noReasonOptionKey)
}
return
}
// Run all deciders and return if they came to a conclusion. // Run all deciders and return if they came to a conclusion.
done, defaultAction := runDeciders(ctx, conn, pkt) done, defaultAction := runDeciders(ctx, defaultDeciders, conn, pkt)
if done { if done {
return return
} }
@ -96,7 +114,7 @@ func DecideOnConnection(ctx context.Context, conn *network.Connection, pkt packe
} }
} }
func runDeciders(ctx context.Context, conn *network.Connection, pkt packet.Packet) (done bool, defaultAction uint8) { func runDeciders(ctx context.Context, selectedDeciders []deciderFn, conn *network.Connection, pkt packet.Packet) (done bool, defaultAction uint8) {
layeredProfile := conn.Process().Profile() layeredProfile := conn.Process().Profile()
// Read-lock the all the profiles. // Read-lock the all the profiles.
@ -104,7 +122,7 @@ func runDeciders(ctx context.Context, conn *network.Connection, pkt packet.Packe
defer layeredProfile.UnlockForUsage() defer layeredProfile.UnlockForUsage()
// Go though all deciders, return if one sets an action. // Go though all deciders, return if one sets an action.
for _, decider := range deciders { for _, decider := range selectedDeciders {
if decider(ctx, conn, pkt) { if decider(ctx, conn, pkt) {
return true, profile.DefaultActionNotSet return true, profile.DefaultActionNotSet
} }
@ -248,39 +266,58 @@ func checkConnectivityDomain(_ context.Context, conn *network.Connection, _ pack
func checkConnectionScope(_ context.Context, conn *network.Connection, _ packet.Packet) bool { func checkConnectionScope(_ context.Context, conn *network.Connection, _ packet.Packet) bool {
p := conn.Process().Profile() p := conn.Process().Profile()
// check scopes // If we are handling a DNS request, check if we can immediately block it.
if conn.Entity.IP != nil { if conn.Type == network.DNSRequest {
classification := netutils.ClassifyIP(conn.Entity.IP)
switch classification {
case netutils.Global, netutils.GlobalMulticast:
if p.BlockScopeInternet() {
conn.Deny("Internet access blocked", profile.CfgOptionBlockScopeInternetKey) // Block Outbound / Drop Inbound
return true
}
case netutils.SiteLocal, netutils.LinkLocal, netutils.LocalMulticast:
if p.BlockScopeLAN() {
conn.Block("LAN access blocked", profile.CfgOptionBlockScopeLANKey) // Block Outbound / Drop Inbound
return true
}
case netutils.HostLocal:
if p.BlockScopeLocal() {
conn.Block("Localhost access blocked", profile.CfgOptionBlockScopeLocalKey) // Block Outbound / Drop Inbound
return true
}
default: // netutils.Invalid
conn.Deny("invalid IP", noReasonOptionKey) // Block Outbound / Drop Inbound
return true
}
} else if conn.Entity.Domain != "" {
// This is a DNS Request.
// DNS is expected to resolve to LAN or Internet addresses. // DNS is expected to resolve to LAN or Internet addresses.
// Localhost queries are immediately responded to by the nameserver. // Localhost queries are immediately responded to by the nameserver.
if p.BlockScopeInternet() && p.BlockScopeLAN() { if p.BlockScopeInternet() && p.BlockScopeLAN() {
conn.Block("Internet and LAN access blocked", profile.CfgOptionBlockScopeInternetKey) conn.Block("Internet and LAN access blocked", profile.CfgOptionBlockScopeInternetKey)
return true return true
} }
return false
} }
// Check if the network scope is permitted.
switch conn.Entity.IPScope {
case netutils.Global, netutils.GlobalMulticast:
if p.BlockScopeInternet() {
conn.Deny("Internet access blocked", profile.CfgOptionBlockScopeInternetKey) // Block Outbound / Drop Inbound
return true
}
case netutils.SiteLocal, netutils.LinkLocal, netutils.LocalMulticast:
if p.BlockScopeLAN() {
conn.Block("LAN access blocked", profile.CfgOptionBlockScopeLANKey) // Block Outbound / Drop Inbound
return true
}
case netutils.HostLocal:
if p.BlockScopeLocal() {
conn.Block("Localhost access blocked", profile.CfgOptionBlockScopeLocalKey) // Block Outbound / Drop Inbound
return true
}
default: // netutils.Unknown and netutils.Invalid
conn.Deny("invalid IP", noReasonOptionKey) // Block Outbound / Drop Inbound
return true
}
// If the IP address was resolved, check the scope of the resolver.
switch {
case p.RemoveOutOfScopeDNS():
// Out of scope checking is not active.
case conn.Resolver == nil:
// IP address of connection was not resolved.
case conn.Resolver.IPScope.IsGlobal() &&
(conn.Entity.IPScope.IsLAN() || conn.Entity.IPScope.IsLocalhost()):
// Block global resolvers from returning LAN/Localhost IPs.
conn.Block("DNS server horizon violation: global DNS server returned local IP address", profile.CfgOptionRemoveOutOfScopeDNSKey)
return true
case conn.Resolver.IPScope.IsLAN() &&
conn.Entity.IPScope.IsLocalhost():
// Block LAN resolvers from returning Localhost IPs.
conn.Block("DNS server horizon violation: LAN DNS server returned localhost IP address", profile.CfgOptionRemoveOutOfScopeDNSKey)
return true
}
return false return false
} }

View file

@ -58,6 +58,9 @@ type Entity struct {
// set, IP has been resolved by following all CNAMEs. // set, IP has been resolved by following all CNAMEs.
IP net.IP IP net.IP
// IPScope holds the network scope of the IP.
IPScope netutils.IPScope
// Country holds the country the IP address (ASN) is // Country holds the country the IP address (ASN) is
// located in. // located in.
Country string Country string
@ -65,6 +68,9 @@ type Entity struct {
// ASN holds the autonomous system number of the IP. // ASN holds the autonomous system number of the IP.
ASN uint ASN uint
// ASOrg holds the owner's name of the autonomous system.
ASOrg string
location *geoip.Location location *geoip.Location
// BlockedByLists holds list source IDs that // BlockedByLists holds list source IDs that
@ -95,6 +101,12 @@ func (e *Entity) Init() *Entity {
return e return e
} }
// SetIP sets the IP address together with its network scope.
func (e *Entity) SetIP(ip net.IP) {
e.IP = ip
e.IPScope = netutils.GetIPScope(ip)
}
// SetDstPort sets the destination port. // SetDstPort sets the destination port.
func (e *Entity) SetDstPort(dstPort uint16) { func (e *Entity) SetDstPort(dstPort uint16) {
e.dstPort = dstPort e.dstPort = dstPort
@ -229,6 +241,7 @@ func (e *Entity) getLocation(ctx context.Context) {
e.location = loc e.location = loc
e.Country = loc.Country.ISOCode e.Country = loc.Country.ISOCode
e.ASN = loc.AutonomousSystemNumber e.ASN = loc.AutonomousSystemNumber
e.ASOrg = loc.AutonomousSystemOrganization
}) })
} }
@ -422,7 +435,7 @@ func (e *Entity) getIPLists(ctx context.Context) {
} }
// only load lists for IP addresses that are classified as global. // only load lists for IP addresses that are classified as global.
if netutils.ClassifyIP(ip) != netutils.Global { if !e.IPScope.IsGlobal() {
return return
} }

View file

@ -3,6 +3,7 @@ package nameserver
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"net" "net"
"strings" "strings"
"time" "time"
@ -57,6 +58,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
log.Warningf("nameserver: failed to get remote address of request for %s%s, ignoring", q.FQDN, q.QType) log.Warningf("nameserver: failed to get remote address of request for %s%s, ignoring", q.FQDN, q.QType)
return nil return nil
} }
// log.Errorf("DEBUG: nameserver: handling new request for %s from %s:%d", q.ID(), remoteAddr.IP, remoteAddr.Port)
// Start context tracer for context-aware logging. // Start context tracer for context-aware logging.
ctx, tracer := log.AddTracer(ctx) ctx, tracer := log.AddTracer(ctx)
@ -100,18 +102,27 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
// Authenticate request - only requests from the local host, but with any of its IPs, are allowed. // Authenticate request - only requests from the local host, but with any of its IPs, are allowed.
local, err := netenv.IsMyIP(remoteAddr.IP) local, err := netenv.IsMyIP(remoteAddr.IP)
if err != nil { if err != nil {
tracer.Warningf("nameserver: failed to check if request for %s%s is local: %s", q.FQDN, q.QType, err) tracer.Warningf("nameserver: failed to check if request for %s is local: %s", q.ID(), err)
return nil // Do no reply, drop request immediately. return nil // Do no reply, drop request immediately.
} }
// Create connection ID for dns request.
connID := fmt.Sprintf(
"%s-%d-#%d-%s",
remoteAddr.IP,
remoteAddr.Port,
request.Id,
q.ID(),
)
// Get connection for this request. This identifies the process behind the request. // Get connection for this request. This identifies the process behind the request.
var conn *network.Connection var conn *network.Connection
switch { switch {
case local: case local:
conn = network.NewConnectionFromDNSRequest(ctx, q.FQDN, nil, remoteAddr.IP, uint16(remoteAddr.Port)) conn = network.NewConnectionFromDNSRequest(ctx, q.FQDN, nil, connID, remoteAddr.IP, uint16(remoteAddr.Port))
case networkServiceMode(): case networkServiceMode():
conn, err = network.NewConnectionFromExternalDNSRequest(ctx, q.FQDN, nil, remoteAddr.IP) conn, err = network.NewConnectionFromExternalDNSRequest(ctx, q.FQDN, nil, connID, remoteAddr.IP)
if err != nil { if err != nil {
tracer.Warningf("nameserver: failed to get host/profile for request for %s%s: %s", q.FQDN, q.QType, err) tracer.Warningf("nameserver: failed to get host/profile for request for %s%s: %s", q.FQDN, q.QType, err)
return nil // Do no reply, drop request immediately. return nil // Do no reply, drop request immediately.
@ -124,20 +135,24 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
conn.Lock() conn.Lock()
defer conn.Unlock() defer conn.Unlock()
// Create reference for the rrCache.
var rrCache *resolver.RRCache
// Once we decided on the connection we might need to save it to the database, // Once we decided on the connection we might need to save it to the database,
// so we defer that check for now. // so we defer that check for now.
defer func() { defer func() {
switch conn.Verdict { switch conn.Verdict {
// We immediately save blocked, dropped or failed verdicts so // We immediately save blocked, dropped or failed verdicts so
// they pop up in the UI. // they pop up in the UI.
case network.VerdictBlock, network.VerdictDrop, network.VerdictFailed: case network.VerdictBlock, network.VerdictDrop, network.VerdictFailed, network.VerdictRerouteToNameserver, network.VerdictRerouteToTunnel:
conn.Save() conn.Save()
// For undecided or accepted connections we don't save them yet, because // For undecided or accepted connections we don't save them yet, because
// that will happen later anyway. // that will happen later anyway.
case network.VerdictUndecided, network.VerdictAccept, case network.VerdictUndecided, network.VerdictAccept:
network.VerdictRerouteToNameserver, network.VerdictRerouteToTunnel: // Save the request as open, as we don't know if there will be a connection or not.
return network.SaveOpenDNSRequest(conn)
firewall.UpdateIPsAndCNAMEs(q, rrCache, conn)
default: default:
tracer.Warningf("nameserver: unexpected verdict %s for connection %s, not saving", conn.Verdict, conn) tracer.Warningf("nameserver: unexpected verdict %s for connection %s, not saving", conn.Verdict, conn)
@ -153,17 +168,19 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
// IP address in which case we "accept" it, but let the firewall handle // IP address in which case we "accept" it, but let the firewall handle
// the resolving as it wishes. // the resolving as it wishes.
if responder, ok := conn.Reason.Context.(nsutil.Responder); ok { if responder, ok := conn.Reason.Context.(nsutil.Responder); ok {
// Save the request as open, as we don't know if there will be a connection or not.
network.SaveOpenDNSRequest(conn)
tracer.Infof("nameserver: handing over request for %s to special filter responder: %s", q.ID(), conn.Reason.Msg) tracer.Infof("nameserver: handing over request for %s to special filter responder: %s", q.ID(), conn.Reason.Msg)
return reply(responder) return reply(responder)
} }
// Check if there is Verdict to act upon. // Check if there is a Verdict to act upon.
switch conn.Verdict { switch conn.Verdict {
case network.VerdictBlock, network.VerdictDrop, network.VerdictFailed: case network.VerdictBlock, network.VerdictDrop, network.VerdictFailed:
tracer.Infof("nameserver: request for %s from %s %s", q.ID(), conn.Process(), conn.Verdict.Verb()) tracer.Infof(
"nameserver: returning %s response for %s to %s",
conn.Verdict.Verb(),
q.ID(),
conn.Process(),
)
return reply(conn, conn) return reply(conn, conn)
} }
@ -171,7 +188,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
q.SecurityLevel = conn.Process().Profile().SecurityLevel() q.SecurityLevel = conn.Process().Profile().SecurityLevel()
// Resolve request. // Resolve request.
rrCache, err := resolver.Resolve(ctx, q) rrCache, err = resolver.Resolve(ctx, q)
if err != nil { if err != nil {
// React to special errors. // React to special errors.
switch { switch {
@ -203,13 +220,10 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
} }
tracer.Trace("nameserver: deciding on resolved dns") tracer.Trace("nameserver: deciding on resolved dns")
rrCache = firewall.DecideOnResolvedDNS(ctx, conn, q, rrCache) rrCache = firewall.FilterResolvedDNS(ctx, conn, q, rrCache)
if rrCache == nil { if rrCache == nil {
// Check again if there is a responder from the firewall. // Check again if there is a responder from the firewall.
if responder, ok := conn.Reason.Context.(nsutil.Responder); ok { if responder, ok := conn.Reason.Context.(nsutil.Responder); ok {
// Save the request as open, as we don't know if there will be a connection or not.
network.SaveOpenDNSRequest(conn)
tracer.Infof("nameserver: handing over request for %s to filter responder: %s", q.ID(), conn.Reason.Msg) tracer.Infof("nameserver: handing over request for %s to filter responder: %s", q.ID(), conn.Reason.Msg)
return reply(responder) return reply(responder)
} }
@ -227,9 +241,6 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
} }
} }
// Save dns request as open.
defer network.SaveOpenDNSRequest(conn)
// Revert back to non-standard question format, if we had to convert. // Revert back to non-standard question format, if we had to convert.
if nonStandardQuestionFormat { if nonStandardQuestionFormat {
rrCache.ReplaceAnswerNames(originalQuestion.Name) rrCache.ReplaceAnswerNames(originalQuestion.Name)

View file

@ -38,12 +38,12 @@ func GetAssignedGlobalAddresses() (ipv4 []net.IP, ipv6 []net.IP, err error) {
return nil, nil, err return nil, nil, err
} }
for _, ip4 := range allv4 { for _, ip4 := range allv4 {
if netutils.IPIsGlobal(ip4) { if netutils.GetIPScope(ip4).IsGlobal() {
ipv4 = append(ipv4, ip4) ipv4 = append(ipv4, ip4)
} }
} }
for _, ip6 := range allv6 { for _, ip6 := range allv6 {
if netutils.IPIsGlobal(ip6) { if netutils.GetIPScope(ip6).IsGlobal() {
ipv6 = append(ipv6, ip6) ipv6 = append(ipv6, ip6)
} }
} }
@ -59,7 +59,7 @@ var (
// Broadcast or multicast addresses will never match, even if valid in in use. // Broadcast or multicast addresses will never match, even if valid in in use.
func IsMyIP(ip net.IP) (yes bool, err error) { func IsMyIP(ip net.IP) (yes bool, err error) {
// Check for IPs that don't need extra checks. // Check for IPs that don't need extra checks.
switch netutils.ClassifyIP(ip) { switch netutils.GetIPScope(ip) {
case netutils.HostLocal: case netutils.HostLocal:
return true, nil return true, nil
case netutils.LocalMulticast, netutils.GlobalMulticast: case netutils.LocalMulticast, netutils.GlobalMulticast:

View file

@ -130,7 +130,7 @@ next:
} }
// If we received something from a global IP address, we have succeeded and can return immediately. // If we received something from a global IP address, we have succeeded and can return immediately.
if netutils.IPIsGlobal(addr.IP) { if netutils.GetIPScope(addr.IP).IsGlobal() {
return addr.IP, nil return addr.IP, nil
} }

View file

@ -356,7 +356,7 @@ func checkOnlineStatus(ctx context.Context) {
} else { } else {
var lan bool var lan bool
for _, ip := range ipv4 { for _, ip := range ipv4 {
switch netutils.ClassifyIP(ip) { switch netutils.GetIPScope(ip) {
case netutils.SiteLocal: case netutils.SiteLocal:
lan = true lan = true
case netutils.Global: case netutils.Global:
@ -366,7 +366,7 @@ func checkOnlineStatus(ctx context.Context) {
} }
} }
for _, ip := range ipv6 { for _, ip := range ipv6 {
switch netutils.ClassifyIP(ip) { switch netutils.GetIPScope(ip) {
case netutils.SiteLocal, netutils.Global: case netutils.SiteLocal, netutils.Global:
// IPv6 global addresses are also used in local networks // IPv6 global addresses are also used in local networks
lan = true lan = true

View file

@ -11,6 +11,8 @@ import (
"github.com/safing/portbase/api" "github.com/safing/portbase/api"
"github.com/safing/portbase/database/query" "github.com/safing/portbase/database/query"
"github.com/safing/portbase/utils/debug" "github.com/safing/portbase/utils/debug"
"github.com/safing/portmaster/network/state"
"github.com/safing/portmaster/process"
"github.com/safing/portmaster/status" "github.com/safing/portmaster/status"
) )
@ -45,6 +47,18 @@ func registerAPIEndpoints() error {
return err return err
} }
if err := api.RegisterEndpoint(api.Endpoint{
Path: "debug/network/state",
Read: api.PermitUser,
StructFunc: func(ar *api.Request) (i interface{}, err error) {
return state.GetInfo(), nil
},
Name: "Get Network State Table Data",
Description: "Returns the current network state tables from the OS.",
}); err != nil {
return err
}
return nil return nil
} }
@ -156,28 +170,30 @@ func AddNetworkDebugData(di *debug.Info, profile, where string) {
func buildNetworkDebugInfoData(debugConns []*Connection) string { func buildNetworkDebugInfoData(debugConns []*Connection) string {
// Sort // Sort
sort.Sort(connectionsByStarted(debugConns)) sort.Sort(connectionsByGroup(debugConns))
// Format lines // Format lines
var buf strings.Builder var buf strings.Builder
currentBinaryPath := "__" currentPID := process.UndefinedProcessID
for _, conn := range debugConns { for _, conn := range debugConns {
conn.Lock() conn.Lock()
// Add process infomration if it differs from previous connection. // Add process infomration if it differs from previous connection.
if currentBinaryPath != conn.ProcessContext.BinaryPath { if currentPID != conn.ProcessContext.PID {
if currentBinaryPath != "__" { if currentPID != process.UndefinedProcessID {
buf.WriteString("\n\n\n") buf.WriteString("\n\n\n")
} }
buf.WriteString("ProcessName: " + conn.ProcessContext.ProcessName) buf.WriteString("ProfileName: " + conn.ProcessContext.ProfileName)
buf.WriteString("\nProfileName: " + conn.ProcessContext.ProfileName)
buf.WriteString("\nBinaryPath: " + conn.ProcessContext.BinaryPath)
buf.WriteString("\nProfile: " + conn.ProcessContext.Profile) buf.WriteString("\nProfile: " + conn.ProcessContext.Profile)
buf.WriteString("\nSource: " + conn.ProcessContext.Source) buf.WriteString("\nSource: " + conn.ProcessContext.Source)
buf.WriteString("\nProcessName: " + conn.ProcessContext.ProcessName)
buf.WriteString("\nBinaryPath: " + conn.ProcessContext.BinaryPath)
buf.WriteString("\nCmdLine: " + conn.ProcessContext.CmdLine)
buf.WriteString("\nPID: " + strconv.Itoa(conn.ProcessContext.PID))
buf.WriteString("\n") buf.WriteString("\n")
// Set current path in order to not print the process information again. // Set current PID in order to not print the process information again.
currentBinaryPath = conn.ProcessContext.BinaryPath currentPID = conn.ProcessContext.PID
} }
// Add connection. // Add connection.
@ -192,7 +208,7 @@ func buildNetworkDebugInfoData(debugConns []*Connection) string {
func (conn *Connection) debugInfoLine() string { func (conn *Connection) debugInfoLine() string {
var connectionData string var connectionData string
if conn.ID != "" { if conn.Type == IPConnection {
// Format IP/Port pair for connections. // Format IP/Port pair for connections.
connectionData = fmt.Sprintf( connectionData = fmt.Sprintf(
"% 15s:%- 5s %s % 15s:%- 5s", "% 15s:%- 5s %s % 15s:%- 5s",
@ -272,13 +288,28 @@ func (conn *Connection) fmtReasonProfileComponent() string {
return conn.Reason.Profile return conn.Reason.Profile
} }
type connectionsByStarted []*Connection type connectionsByGroup []*Connection
func (a connectionsByStarted) Len() int { return len(a) } func (a connectionsByGroup) Len() int { return len(a) }
func (a connectionsByStarted) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a connectionsByGroup) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a connectionsByStarted) Less(i, j int) bool { func (a connectionsByGroup) Less(i, j int) bool {
// Sort by:
// 1. Profile ID
if a[i].ProcessContext.Profile != a[j].ProcessContext.Profile {
return a[i].ProcessContext.Profile < a[j].ProcessContext.Profile
}
// 2. Process Binary
if a[i].ProcessContext.BinaryPath != a[j].ProcessContext.BinaryPath { if a[i].ProcessContext.BinaryPath != a[j].ProcessContext.BinaryPath {
return a[i].ProcessContext.BinaryPath < a[j].ProcessContext.BinaryPath return a[i].ProcessContext.BinaryPath < a[j].ProcessContext.BinaryPath
} }
// 3. Process ID
if a[i].ProcessContext.PID != a[j].ProcessContext.PID {
return a[i].ProcessContext.PID < a[j].ProcessContext.PID
}
// 4. Started
return a[i].Started < a[j].Started return a[i].Started < a[j].Started
} }

View file

@ -29,10 +29,12 @@ type FirewallHandler func(conn *Connection, pkt packet.Packet)
type ProcessContext struct { type ProcessContext struct {
// ProcessName is the name of the process. // ProcessName is the name of the process.
ProcessName string ProcessName string
//ProfileName is the name of the profile. // ProfileName is the name of the profile.
ProfileName string ProfileName string
// BinaryPath is the path to the process binary. // BinaryPath is the path to the process binary.
BinaryPath string BinaryPath string
// CmdLine holds the execution parameters.
CmdLine string
// PID is the process identifier. // PID is the process identifier.
PID int PID int
// Profile is the ID of the main profile that // Profile is the ID of the main profile that
@ -42,21 +44,37 @@ type ProcessContext struct {
Source string Source string
} }
type ConnectionType int8
const (
Undefined ConnectionType = iota
IPConnection
DNSRequest
// ProxyRequest
)
// Connection describes a distinct physical network connection // Connection describes a distinct physical network connection
// identified by the IP/Port pair. // identified by the IP/Port pair.
type Connection struct { //nolint:maligned // TODO: fix alignment type Connection struct { //nolint:maligned // TODO: fix alignment
record.Base record.Base
sync.Mutex sync.Mutex
// ID may hold unique connection id. It is only set for non-DNS // ID holds a unique request/connection id and is considered immutable after
// request connections and is considered immutable after a // creation.
// connection object has been created.
ID string ID string
// Type defines the connection type.
Type ConnectionType
// External defines if the connection represents an external request or
// connection.
External bool
// Scope defines the scope of a connection. For DNS requests, the // Scope defines the scope of a connection. For DNS requests, the
// scope is always set to the domain name. For direct packet // scope is always set to the domain name. For direct packet
// connections the scope consists of the involved network environment // connections the scope consists of the involved network environment
// and the packet direction. Once a connection object is created, // and the packet direction. Once a connection object is created,
// Scope is considered immutable. // Scope is considered immutable.
// Deprecated: This field holds duplicate information, which is accessible
// clearer through other attributes. Please use conn.Type, conn.Inbound
// and conn.Entity.Domain instead.
Scope string Scope string
// IPVersion is set to the packet IP version. It is not set (0) for // IPVersion is set to the packet IP version. It is not set (0) for
// connections created from a DNS request. // connections created from a DNS request.
@ -74,6 +92,8 @@ type Connection struct { //nolint:maligned // TODO: fix alignment
// set for connections created from DNS requests. LocalIP is // set for connections created from DNS requests. LocalIP is
// considered immutable once a connection object has been created. // considered immutable once a connection object has been created.
LocalIP net.IP LocalIP net.IP
// LocalIPScope holds the network scope of the local IP.
LocalIPScope netutils.IPScope
// LocalPort holds the local port of the connection. It is not // LocalPort holds the local port of the connection. It is not
// set for connections created from DNS requests. LocalPort is // set for connections created from DNS requests. LocalPort is
// considered immutable once a connection object has been created. // considered immutable once a connection object has been created.
@ -83,6 +103,9 @@ type Connection struct { //nolint:maligned // TODO: fix alignment
// be added to it during the livetime of a connection. Access to // be added to it during the livetime of a connection. Access to
// entity must be guarded by the connection lock. // entity must be guarded by the connection lock.
Entity *intel.Entity Entity *intel.Entity
// Resolver holds information about the resolver used to resolve
// Entity.Domain.
Resolver *resolver.ResolverInfo
// Verdict is the final decision that has been made for a connection. // Verdict is the final decision that has been made for a connection.
// The verdict may change so any access to it must be guarded by the // The verdict may change so any access to it must be guarded by the
// connection lock. // connection lock.
@ -171,8 +194,9 @@ type Reason struct {
func getProcessContext(ctx context.Context, proc *process.Process) ProcessContext { func getProcessContext(ctx context.Context, proc *process.Process) ProcessContext {
// Gather process information. // Gather process information.
pCtx := ProcessContext{ pCtx := ProcessContext{
BinaryPath: proc.Path,
ProcessName: proc.Name, ProcessName: proc.Name,
BinaryPath: proc.Path,
CmdLine: proc.CmdLine,
PID: proc.Pid, PID: proc.Pid,
} }
@ -191,7 +215,7 @@ func getProcessContext(ctx context.Context, proc *process.Process) ProcessContex
} }
// NewConnectionFromDNSRequest returns a new connection based on the given dns request. // NewConnectionFromDNSRequest returns a new connection based on the given dns request.
func NewConnectionFromDNSRequest(ctx context.Context, fqdn string, cnames []string, localIP net.IP, localPort uint16) *Connection { func NewConnectionFromDNSRequest(ctx context.Context, fqdn string, cnames []string, connID string, localIP net.IP, localPort uint16) *Connection {
// Determine IP version. // Determine IP version.
ipVersion := packet.IPv6 ipVersion := packet.IPv6
if localIP.To4() != nil { if localIP.To4() != nil {
@ -218,6 +242,8 @@ func NewConnectionFromDNSRequest(ctx context.Context, fqdn string, cnames []stri
timestamp := time.Now().Unix() timestamp := time.Now().Unix()
dnsConn := &Connection{ dnsConn := &Connection{
ID: connID,
Type: DNSRequest,
Scope: fqdn, Scope: fqdn,
Entity: &intel.Entity{ Entity: &intel.Entity{
Domain: fqdn, Domain: fqdn,
@ -234,10 +260,15 @@ func NewConnectionFromDNSRequest(ctx context.Context, fqdn string, cnames []stri
dnsConn.Internal = localProfile.Internal dnsConn.Internal = localProfile.Internal
} }
// Always mark dns queries from the system resolver as internal.
if proc.IsSystemResolver() {
dnsConn.Internal = true
}
return dnsConn return dnsConn
} }
func NewConnectionFromExternalDNSRequest(ctx context.Context, fqdn string, cnames []string, remoteIP net.IP) (*Connection, error) { func NewConnectionFromExternalDNSRequest(ctx context.Context, fqdn string, cnames []string, connID string, remoteIP net.IP) (*Connection, error) {
remoteHost, err := process.GetNetworkHost(ctx, remoteIP) remoteHost, err := process.GetNetworkHost(ctx, remoteIP)
if err != nil { if err != nil {
return nil, err return nil, err
@ -245,7 +276,10 @@ func NewConnectionFromExternalDNSRequest(ctx context.Context, fqdn string, cname
timestamp := time.Now().Unix() timestamp := time.Now().Unix()
dnsConn := &Connection{ dnsConn := &Connection{
Scope: fqdn, ID: connID,
Type: DNSRequest,
External: true,
Scope: fqdn,
Entity: &intel.Entity{ Entity: &intel.Entity{
Domain: fqdn, Domain: fqdn,
CNAME: cnames, CNAME: cnames,
@ -275,11 +309,19 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection {
var scope string var scope string
var entity *intel.Entity var entity *intel.Entity
var resolverInfo *resolver.ResolverInfo
if inbound { if inbound {
// inbound connection // inbound connection
switch netutils.ClassifyIP(pkt.Info().Src) { entity = &intel.Entity{
Protocol: uint8(pkt.Info().Protocol),
Port: pkt.Info().SrcPort,
}
entity.SetIP(pkt.Info().Src)
entity.SetDstPort(pkt.Info().DstPort)
switch entity.IPScope {
case netutils.HostLocal: case netutils.HostLocal:
scope = IncomingHost scope = IncomingHost
case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast: case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast:
@ -292,31 +334,30 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection {
default: default:
scope = IncomingInvalid scope = IncomingInvalid
} }
entity = &intel.Entity{
IP: pkt.Info().Src,
Protocol: uint8(pkt.Info().Protocol),
Port: pkt.Info().SrcPort,
}
entity.SetDstPort(pkt.Info().DstPort)
} else { } else {
// outbound connection // outbound connection
entity = &intel.Entity{ entity = &intel.Entity{
IP: pkt.Info().Dst,
Protocol: uint8(pkt.Info().Protocol), Protocol: uint8(pkt.Info().Protocol),
Port: pkt.Info().DstPort, Port: pkt.Info().DstPort,
} }
entity.SetIP(pkt.Info().Dst)
entity.SetDstPort(entity.Port) entity.SetDstPort(entity.Port)
// check if we can find a domain for that IP // check if we can find a domain for that IP
ipinfo, err := resolver.GetIPInfo(proc.LocalProfileKey, pkt.Info().Dst.String()) ipinfo, err := resolver.GetIPInfo(proc.Profile().LocalProfile().ID, pkt.Info().Dst.String())
if err != nil {
// Try again with the global scope, in case DNS went through the system resolver.
ipinfo, err = resolver.GetIPInfo(resolver.IPInfoProfileScopeGlobal, pkt.Info().Dst.String())
}
if err == nil { if err == nil {
lastResolvedDomain := ipinfo.MostRecentDomain() lastResolvedDomain := ipinfo.MostRecentDomain()
if lastResolvedDomain != nil { if lastResolvedDomain != nil {
scope = lastResolvedDomain.Domain scope = lastResolvedDomain.Domain
entity.Domain = lastResolvedDomain.Domain entity.Domain = lastResolvedDomain.Domain
entity.CNAME = lastResolvedDomain.CNAMEs entity.CNAME = lastResolvedDomain.CNAMEs
resolverInfo = lastResolvedDomain.Resolver
removeOpenDNSRequest(proc.Pid, lastResolvedDomain.Domain) removeOpenDNSRequest(proc.Pid, lastResolvedDomain.Domain)
} }
} }
@ -331,7 +372,7 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection {
if scope == "" { if scope == "" {
// outbound direct (possibly P2P) connection // outbound direct (possibly P2P) connection
switch netutils.ClassifyIP(pkt.Info().Dst) { switch entity.IPScope {
case netutils.HostLocal: case netutils.HostLocal:
scope = PeerHost scope = PeerHost
case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast: case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast:
@ -351,21 +392,24 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection {
// Create new connection object. // Create new connection object.
newConn := &Connection{ newConn := &Connection{
ID: pkt.GetConnectionID(), ID: pkt.GetConnectionID(),
Type: IPConnection,
Scope: scope, Scope: scope,
IPVersion: pkt.Info().Version, IPVersion: pkt.Info().Version,
Inbound: inbound, Inbound: inbound,
// local endpoint // local endpoint
IPProtocol: pkt.Info().Protocol, IPProtocol: pkt.Info().Protocol,
LocalIP: pkt.Info().LocalIP(),
LocalPort: pkt.Info().LocalPort(), LocalPort: pkt.Info().LocalPort(),
ProcessContext: getProcessContext(pkt.Ctx(), proc), ProcessContext: getProcessContext(pkt.Ctx(), proc),
process: proc, process: proc,
// remote endpoint // remote endpoint
Entity: entity, Entity: entity,
// resolver used to resolve dns request
Resolver: resolverInfo,
// meta // meta
Started: time.Now().Unix(), Started: time.Now().Unix(),
ProfileRevisionCounter: proc.Profile().RevisionCnt(), ProfileRevisionCounter: proc.Profile().RevisionCnt(),
} }
newConn.SetLocalIP(pkt.Info().LocalIP())
// Inherit internal status of profile. // Inherit internal status of profile.
if localProfile := proc.Profile().LocalProfile(); localProfile != nil { if localProfile := proc.Profile().LocalProfile(); localProfile != nil {
@ -380,6 +424,13 @@ func GetConnection(id string) (*Connection, bool) {
return conns.get(id) return conns.get(id)
} }
// SetLocalIP sets the local IP address together with its network scope. The
// connection is not locked for this.
func (conn *Connection) SetLocalIP(ip net.IP) {
conn.LocalIP = ip
conn.LocalIPScope = netutils.GetIPScope(ip)
}
// AcceptWithContext accepts the connection. // AcceptWithContext accepts the connection.
func (conn *Connection) AcceptWithContext(reason, reasonOptionKey string, ctx interface{}) { func (conn *Connection) AcceptWithContext(reason, reasonOptionKey string, ctx interface{}) {
if !conn.SetVerdict(VerdictAccept, reason, reasonOptionKey, ctx) { if !conn.SetVerdict(VerdictAccept, reason, reasonOptionKey, ctx) {
@ -477,14 +528,11 @@ func (conn *Connection) Save() {
conn.UpdateMeta() conn.UpdateMeta()
if !conn.KeyIsSet() { if !conn.KeyIsSet() {
// A connection without an ID has been created from if conn.Type == DNSRequest {
// a DNS request rather than a packet. Choose the correct conn.SetKey(makeKey(conn.process.Pid, "dns", conn.ID))
// connection store here.
if conn.ID == "" {
conn.SetKey(fmt.Sprintf("network:tree/%d/%s", conn.process.Pid, conn.Scope))
dnsConns.add(conn) dnsConns.add(conn)
} else { } else {
conn.SetKey(fmt.Sprintf("network:tree/%d/%s/%s", conn.process.Pid, conn.Scope, conn.ID)) conn.SetKey(makeKey(conn.process.Pid, "ip", conn.ID))
conns.add(conn) conns.add(conn)
} }
} }
@ -500,10 +548,10 @@ func (conn *Connection) delete() {
// A connection without an ID has been created from // A connection without an ID has been created from
// a DNS request rather than a packet. Choose the correct // a DNS request rather than a packet. Choose the correct
// connection store here. // connection store here.
if conn.ID == "" { if conn.Type == IPConnection {
dnsConns.delete(conn)
} else {
conns.delete(conn) conns.delete(conn)
} else {
dnsConns.delete(conn)
} }
conn.Meta().Delete() conn.Meta().Delete()

View file

@ -1,7 +1,6 @@
package network package network
import ( import (
"strconv"
"sync" "sync"
) )
@ -16,25 +15,18 @@ func newConnectionStore() *connectionStore {
} }
} }
func (cs *connectionStore) getID(conn *Connection) string {
if conn.ID != "" {
return conn.ID
}
return strconv.Itoa(conn.process.Pid) + "/" + conn.Scope
}
func (cs *connectionStore) add(conn *Connection) { func (cs *connectionStore) add(conn *Connection) {
cs.rw.Lock() cs.rw.Lock()
defer cs.rw.Unlock() defer cs.rw.Unlock()
cs.items[cs.getID(conn)] = conn cs.items[conn.ID] = conn
} }
func (cs *connectionStore) delete(conn *Connection) { func (cs *connectionStore) delete(conn *Connection) {
cs.rw.Lock() cs.rw.Lock()
defer cs.rw.Unlock() defer cs.rw.Unlock()
delete(cs.items, cs.getID(conn)) delete(cs.items, conn.ID)
} }
func (cs *connectionStore) get(id string) (*Connection, bool) { func (cs *connectionStore) get(id string) (*Connection, bool) {

View file

@ -1,11 +1,10 @@
package network package network
import ( import (
"fmt"
"strconv" "strconv"
"strings" "strings"
"github.com/safing/portmaster/network/state"
"github.com/safing/portbase/database" "github.com/safing/portbase/database"
"github.com/safing/portbase/database/iterator" "github.com/safing/portbase/database/iterator"
"github.com/safing/portbase/database/query" "github.com/safing/portbase/database/query"
@ -27,37 +26,86 @@ type StorageInterface struct {
storage.InjectBase storage.InjectBase
} }
// Get returns a database record. // Database prefixes:
func (s *StorageInterface) Get(key string) (record.Record, error) { // Processes: network:tree/<PID>
// DNS Requests: network:tree/<PID>/dns/<ID>
// IP Connections: network:tree/<PID>/ip/<ID>
splitted := strings.Split(key, "/") func makeKey(pid int, scope, id string) string {
switch splitted[0] { //nolint:gocritic // TODO: implement full key space if scope == "" {
case "tree": return "network:tree/" + strconv.Itoa(pid)
switch len(splitted) { }
case 2: return fmt.Sprintf("network:tree/%d/%s/%s", pid, scope, id)
pid, err := strconv.Atoi(splitted[1]) }
if err == nil {
proc, ok := process.GetProcessFromStorage(pid) func parseDBKey(key string) (pid int, scope, id string, ok bool) {
if ok { // Split into segments.
return proc, nil segments := strings.Split(key, "/")
} // Check for valid prefix.
} if !strings.HasPrefix("tree", segments[0]) {
case 3: return 0, "", "", false
if r, ok := dnsConns.get(splitted[1] + "/" + splitted[2]); ok { }
return r, nil
} // Keys have 2 or 4 segments.
case 4: switch len(segments) {
if r, ok := conns.get(splitted[3]); ok { case 4:
return r, nil id = segments[3]
fallthrough
case 3:
scope = segments[2]
// Sanity check.
switch scope {
case "dns", "ip", "":
// Parsed id matches possible values.
// The empty string is for matching a trailing slash for in query prefix.
// TODO: For queries, also prefixes of these values are valid.
default:
// Unknown scope.
return 0, "", "", false
}
fallthrough
case 2:
var err error
if segments[1] == "" {
pid = process.UndefinedProcessID
} else {
pid, err = strconv.Atoi(segments[1])
if err != nil {
return 0, "", "", false
} }
} }
case "system":
if len(splitted) >= 2 { return pid, scope, id, true
switch splitted[1] { case 1:
case "state": // This is a valid query prefix, but not process ID was given.
return state.GetInfo(), nil return process.UndefinedProcessID, "", "", true
default: default:
} return 0, "", "", false
}
}
// Get returns a database record.
func (s *StorageInterface) Get(key string) (record.Record, error) {
// Parse key and check if valid.
pid, scope, id, ok := parseDBKey(strings.TrimPrefix(key, "network:"))
if !ok || pid == process.UndefinedProcessID {
return nil, storage.ErrNotFound
}
switch scope {
case "dns":
if r, ok := dnsConns.get(id); ok {
return r, nil
}
case "ip":
if r, ok := conns.get(id); ok {
return r, nil
}
case "":
if proc, ok := process.GetProcessFromStorage(pid); ok {
return proc, nil
} }
} }
@ -74,9 +122,13 @@ func (s *StorageInterface) Query(q *query.Query, local, internal bool) (*iterato
} }
func (s *StorageInterface) processQuery(q *query.Query, it *iterator.Iterator) { func (s *StorageInterface) processQuery(q *query.Query, it *iterator.Iterator) {
slashes := strings.Count(q.DatabaseKeyPrefix(), "/") pid, scope, _, ok := parseDBKey(q.DatabaseKeyPrefix())
if !ok {
it.Finish(nil)
return
}
if slashes <= 1 { if pid == process.UndefinedProcessID {
// processes // processes
for _, proc := range process.All() { for _, proc := range process.All() {
proc.Lock() proc.Lock()
@ -87,7 +139,7 @@ func (s *StorageInterface) processQuery(q *query.Query, it *iterator.Iterator) {
} }
} }
if slashes <= 2 { if scope == "" || scope == "dns" {
// dns scopes only // dns scopes only
for _, dnsConn := range dnsConns.clone() { for _, dnsConn := range dnsConns.clone() {
dnsConn.Lock() dnsConn.Lock()
@ -98,7 +150,7 @@ func (s *StorageInterface) processQuery(q *query.Query, it *iterator.Iterator) {
} }
} }
if slashes <= 3 { if scope == "" || scope == "ip" {
// connections // connections
for _, conn := range conns.clone() { for _, conn := range conns.clone() {
conn.Lock() conn.Lock()

View file

@ -132,7 +132,7 @@ func (conn *Connection) addToMetrics() {
} }
// Only count successful connections, not DNS requests. // Only count successful connections, not DNS requests.
if conn.ID == "" { if conn.Type == DNSRequest {
return return
} }

View file

@ -2,19 +2,29 @@ package netutils
import "net" import "net"
// IP classifications // IPScope is the scope of the IP address.
type IPScope int8
// Defined IP Scopes.
const ( const (
HostLocal int8 = iota Invalid IPScope = iota - 1
Undefined
HostLocal
LinkLocal LinkLocal
SiteLocal SiteLocal
Global Global
LocalMulticast LocalMulticast
GlobalMulticast GlobalMulticast
Invalid int8 = -1
) )
// ClassifyIP returns the classification for the given IP address. // ClassifyIP returns the network scope of the given IP address.
func ClassifyIP(ip net.IP) int8 { //nolint:gocognit // Deprecated: Please use the new GetIPScope instead.
func ClassifyIP(ip net.IP) IPScope {
return GetIPScope(ip)
}
// GetIPScope returns the network scope of the given IP address.
func GetIPScope(ip net.IP) IPScope { //nolint:gocognit
if ip4 := ip.To4(); ip4 != nil { if ip4 := ip.To4(); ip4 != nil {
// IPv4 // IPv4
switch { switch {
@ -76,32 +86,27 @@ func ClassifyIP(ip net.IP) int8 { //nolint:gocognit
return Invalid return Invalid
} }
// IPIsLocalhost returns whether the IP refers to the host itself. // IsLocalhost returns whether the IP refers to the host itself.
func IPIsLocalhost(ip net.IP) bool { func (scope IPScope) IsLocalhost() bool {
return ClassifyIP(ip) == HostLocal return scope == HostLocal
} }
// IPIsLAN returns true if the given IP is a site-local or link-local address. // IsLAN returns true if the scope is site-local or link-local.
func IPIsLAN(ip net.IP) bool { func (scope IPScope) IsLAN() bool {
switch ClassifyIP(ip) { switch scope {
case SiteLocal, LinkLocal: case SiteLocal, LinkLocal, LocalMulticast:
return true return true
default: default:
return false return false
} }
} }
// IPIsGlobal returns true if the given IP is a global address. // IsGlobal returns true if the scope is global.
func IPIsGlobal(ip net.IP) bool { func (scope IPScope) IsGlobal() bool {
return ClassifyIP(ip) == Global switch scope {
} case Global, GlobalMulticast:
return true
// IPIsLinkLocal returns true if the given IP is a link-local address. default:
func IPIsLinkLocal(ip net.IP) bool { return false
return ClassifyIP(ip) == LinkLocal }
}
// IPIsSiteLocal returns true if the given IP is a site-local address.
func IPIsSiteLocal(ip net.IP) bool {
return ClassifyIP(ip) == SiteLocal
} }

View file

@ -5,26 +5,30 @@ import (
"testing" "testing"
) )
func TestIPClassification(t *testing.T) { func TestIPScope(t *testing.T) {
testClassification(t, net.IPv4(71, 87, 113, 211), Global) testScope(t, net.IPv4(71, 87, 113, 211), Global)
testClassification(t, net.IPv4(127, 0, 0, 1), HostLocal) testScope(t, net.IPv4(127, 0, 0, 1), HostLocal)
testClassification(t, net.IPv4(127, 255, 255, 1), HostLocal) testScope(t, net.IPv4(127, 255, 255, 1), HostLocal)
testClassification(t, net.IPv4(192, 168, 172, 24), SiteLocal) testScope(t, net.IPv4(192, 168, 172, 24), SiteLocal)
testClassification(t, net.IPv4(172, 15, 1, 1), Global) testScope(t, net.IPv4(172, 15, 1, 1), Global)
testClassification(t, net.IPv4(172, 16, 1, 1), SiteLocal) testScope(t, net.IPv4(172, 16, 1, 1), SiteLocal)
testClassification(t, net.IPv4(172, 31, 1, 1), SiteLocal) testScope(t, net.IPv4(172, 31, 1, 1), SiteLocal)
testClassification(t, net.IPv4(172, 32, 1, 1), Global) testScope(t, net.IPv4(172, 32, 1, 1), Global)
} }
func testClassification(t *testing.T, ip net.IP, expectedClassification int8) { func testScope(t *testing.T, ip net.IP, expectedScope IPScope) {
c := ClassifyIP(ip) c := GetIPScope(ip)
if c != expectedClassification { if c != expectedScope {
t.Errorf("%s is %s, expected %s", ip, classificationString(c), classificationString(expectedClassification)) t.Errorf("%s is %s, expected %s", ip, scopeName(c), scopeName(expectedScope))
} }
} }
func classificationString(c int8) string { func scopeName(c IPScope) string {
switch c { switch c {
case Invalid:
return "invalid"
case Undefined:
return "undefined"
case HostLocal: case HostLocal:
return "hostLocal" return "hostLocal"
case LinkLocal: case LinkLocal:
@ -37,9 +41,7 @@ func classificationString(c int8) string {
return "localMulticast" return "localMulticast"
case GlobalMulticast: case GlobalMulticast:
return "globalMulticast" return "globalMulticast"
case Invalid:
return "invalid"
default: default:
return "unknown" return "undefined"
} }
} }

View file

@ -151,7 +151,7 @@ func (table *udpTable) lookup(pktInfo *packet.Info, fast bool) (
// attribute an incoming broadcast/multicast packet to the wrong process if // attribute an incoming broadcast/multicast packet to the wrong process if
// there are multiple processes listening on the same local port, but // there are multiple processes listening on the same local port, but
// binding to different addresses. This highly unusual for clients. // binding to different addresses. This highly unusual for clients.
isInboundMulticast := pktInfo.Inbound && netutils.ClassifyIP(pktInfo.LocalIP()) == netutils.LocalMulticast isInboundMulticast := pktInfo.Inbound && netutils.GetIPScope(pktInfo.LocalIP()) == netutils.LocalMulticast
// Search for the socket until found. // Search for the socket until found.
for i := 1; i <= lookupRetries; i++ { for i := 1; i <= lookupRetries; i++ {

View file

@ -17,7 +17,7 @@ func registerConfiguration() error {
err := config.Register(&config.Option{ err := config.Register(&config.Option{
Name: "Process Detection", Name: "Process Detection",
Key: CfgOptionEnableProcessDetectionKey, Key: CfgOptionEnableProcessDetectionKey,
Description: "This option enables the attribution of network traffic to processes. This should always be enabled, and effectively disables app settings if disabled.", Description: "This option enables the attribution of network traffic to processes. Without it, app settings are effectively disabled.",
OptType: config.OptTypeBool, OptType: config.OptTypeBool,
ExpertiseLevel: config.ExpertiseLevelDeveloper, ExpertiseLevel: config.ExpertiseLevelDeveloper,
DefaultValue: true, DefaultValue: true,

View file

@ -44,6 +44,10 @@ type Process struct {
CmdLine string CmdLine string
FirstArg string FirstArg string
// SpecialDetail holds special information, the meaning of which can change
// based on any of the previous attributes.
SpecialDetail string
LocalProfileKey string LocalProfileKey string
profile *profile.LayeredProfile profile *profile.LayeredProfile
@ -65,6 +69,24 @@ func (p *Process) Profile() *profile.LayeredProfile {
return p.profile return p.profile
} }
// IsSystemResolver is a shortcut to check if the process is or belongs to the
// system resolver and needs special handling.
func (p *Process) IsSystemResolver() bool {
// Check if process exists.
if p == nil {
return false
}
// Check if local profile exists.
localProfile := p.profile.LocalProfile()
if localProfile == nil {
return false
}
// Check ID.
return localProfile.ID == profile.SystemResolverProfileID
}
// GetLastSeen returns the unix timestamp when the process was last seen. // GetLastSeen returns the unix timestamp when the process was last seen.
func (p *Process) GetLastSeen() int64 { func (p *Process) GetLastSeen() int64 {
p.Lock() p.Lock()

View file

@ -18,6 +18,7 @@ func (p *Process) specialOSInit() {
switch err { switch err {
case nil: case nil:
p.Name += fmt.Sprintf(" (%s)", svcNames) p.Name += fmt.Sprintf(" (%s)", svcNames)
p.SpecialDetail = svcNames
case osdetail.ErrServiceNotFound: case osdetail.ErrServiceNotFound:
log.Tracef("process: failed to get service name for svchost.exe (pid %d): %s", p.Pid, err) log.Tracef("process: failed to get service name for svchost.exe (pid %d): %s", p.Pid, err)
default: default:

View file

@ -3,6 +3,7 @@ package process
import ( import (
"context" "context"
"os" "os"
"runtime"
"strings" "strings"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
@ -54,6 +55,22 @@ func (p *Process) GetProfile(ctx context.Context) (changed bool, err error) {
// sure that we won't kill any of our own things. // sure that we won't kill any of our own things.
} }
} }
// Check if this is the system resolver.
switch runtime.GOOS {
case "windows":
if (p.Path == `C:\Windows\System32\svchost.exe` || p.Path == `C:\Windows\system32\svchost.exe`) &&
(strings.Contains(p.SpecialDetail, "Dnscache") || strings.Contains(p.CmdLine, "-k NetworkService")) {
profileID = profile.SystemResolverProfileID
}
case "linux":
switch p.Path {
case "/lib/systemd/systemd-resolved",
"/usr/lib/systemd/systemd-resolved",
"/lib64/systemd/systemd-resolved",
"/usr/lib64/systemd/systemd-resolved":
profileID = profile.SystemResolverProfileID
}
}
} }
// Get the (linked) local profile. // Get the (linked) local profile.

View file

@ -14,6 +14,10 @@ const (
// attributed to a PID for any reason. // attributed to a PID for any reason.
UnidentifiedProcessID = -1 UnidentifiedProcessID = -1
// UndefinedProcessID is not used by any (virtual) process and signifies that
// the PID is unset.
UndefinedProcessID = -2
// NetworkHostProcessID is the PID used for requests served to the network. // NetworkHostProcessID is the PID used for requests served to the network.
NetworkHostProcessID = -255 NetworkHostProcessID = -255
) )

View file

@ -434,7 +434,7 @@ The lists are automatically updated every hour using incremental updates.
err = config.Register(&config.Option{ err = config.Register(&config.Option{
Name: "Enforce Global/Private Split-View", Name: "Enforce Global/Private Split-View",
Key: CfgOptionRemoveOutOfScopeDNSKey, Key: CfgOptionRemoveOutOfScopeDNSKey,
Description: "Reject private IP addresses (RFC1918 et al.) from public DNS responses.", Description: "Reject private IP addresses (RFC1918 et al.) from public DNS responses. If the system resolver is in use, the resulting connection will be blocked instead of the DNS request.",
OptType: config.OptTypeInt, OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelDeveloper, ExpertiseLevel: config.ExpertiseLevelDeveloper,
DefaultValue: status.SecurityLevelsAll, DefaultValue: status.SecurityLevelsAll,
@ -455,7 +455,7 @@ The lists are automatically updated every hour using incremental updates.
err = config.Register(&config.Option{ err = config.Register(&config.Option{
Name: "Reject Blocked IPs", Name: "Reject Blocked IPs",
Key: CfgOptionRemoveBlockedDNSKey, Key: CfgOptionRemoveBlockedDNSKey,
Description: "Reject blocked IP addresses directly from the DNS response instead of handing them over to the app and blocking a resulting connection.", Description: "Reject blocked IP addresses directly from the DNS response instead of handing them over to the app and blocking a resulting connection. This settings does not affect privacy and only takes effect when the system resolver is not in use.",
OptType: config.OptTypeInt, OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelDeveloper, ExpertiseLevel: config.ExpertiseLevelDeveloper,
DefaultValue: status.SecurityLevelsAll, DefaultValue: status.SecurityLevelsAll,
@ -491,6 +491,7 @@ The lists are automatically updated every hour using incremental updates.
return err return err
} }
cfgOptionDomainHeuristics = config.Concurrent.GetAsInt(CfgOptionDomainHeuristicsKey, int64(status.SecurityLevelsAll)) cfgOptionDomainHeuristics = config.Concurrent.GetAsInt(CfgOptionDomainHeuristicsKey, int64(status.SecurityLevelsAll))
cfgIntOptions[CfgOptionDomainHeuristicsKey] = cfgOptionDomainHeuristics
// Bypass prevention // Bypass prevention
err = config.Register(&config.Option{ err = config.Register(&config.Option{
@ -499,7 +500,9 @@ The lists are automatically updated every hour using incremental updates.
Description: `Prevent apps from bypassing the privacy filter. Description: `Prevent apps from bypassing the privacy filter.
Current Features: Current Features:
- Disable Firefox' internal DNS-over-HTTPs resolver - Disable Firefox' internal DNS-over-HTTPs resolver
- Block direct access to public DNS resolvers`, - Block direct access to public DNS resolvers
Please note that if you are using the system resolver, bypass attempts might be additionally blocked there too.`,
OptType: config.OptTypeInt, OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelUser, ExpertiseLevel: config.ExpertiseLevelUser,
ReleaseLevel: config.ReleaseLevelBeta, ReleaseLevel: config.ReleaseLevelBeta,

View file

@ -36,9 +36,8 @@ func (ep *EndpointScope) Matches(_ context.Context, entity *intel.Entity) (EPRes
return Undeterminable, nil return Undeterminable, nil
} }
classification := netutils.ClassifyIP(entity.IP)
var scope uint8 var scope uint8
switch classification { switch entity.IPScope {
case netutils.HostLocal: case netutils.HostLocal:
scope = scopeLocalhost scope = scopeLocalhost
case netutils.LinkLocal: case netutils.LinkLocal:

View file

@ -11,6 +11,11 @@ const (
// SystemProfileName is the name used for the system/kernel. // SystemProfileName is the name used for the system/kernel.
SystemProfileName = "Operating System" SystemProfileName = "Operating System"
// SystemResolverProfileID is the profile ID used for the system's DNS resolver.
SystemResolverProfileID = "_system-resolver"
// SystemResolverProfileName is the name used for the system's DNS resolver.
SystemResolverProfileName = "System DNS Client"
// PortmasterProfileID is the profile ID used for the Portmaster Core itself. // PortmasterProfileID is the profile ID used for the Portmaster Core itself.
PortmasterProfileID = "_portmaster" PortmasterProfileID = "_portmaster"
// PortmasterProfileName is the name used for the Portmaster Core itself. // PortmasterProfileName is the name used for the Portmaster Core itself.
@ -35,6 +40,8 @@ func updateSpecialProfileMetadata(profile *Profile, binaryPath string) (ok, chan
newProfileName = UnidentifiedProfileName newProfileName = UnidentifiedProfileName
case SystemProfileID: case SystemProfileID:
newProfileName = SystemProfileName newProfileName = SystemProfileName
case SystemResolverProfileID:
newProfileName = SystemResolverProfileName
case PortmasterProfileID: case PortmasterProfileID:
newProfileName = PortmasterProfileName newProfileName = PortmasterProfileName
case PortmasterAppProfileID: case PortmasterAppProfileID:
@ -68,6 +75,9 @@ func getSpecialProfile(profileID, linkedPath string) *Profile {
case SystemProfileID: case SystemProfileID:
return New(SourceLocal, SystemProfileID, linkedPath, nil) return New(SourceLocal, SystemProfileID, linkedPath, nil)
case SystemResolverProfileID:
return New(SourceLocal, SystemResolverProfileID, linkedPath, nil)
case PortmasterProfileID: case PortmasterProfileID:
profile := New(SourceLocal, PortmasterProfileID, linkedPath, nil) profile := New(SourceLocal, PortmasterProfileID, linkedPath, nil)
profile.Internal = true profile.Internal = true

View file

@ -40,6 +40,10 @@ type ResolvedDomain struct {
// Domain. // Domain.
CNAMEs []string CNAMEs []string
// Resolver holds basic information about the resolver that provided this
// information.
Resolver *ResolverInfo
// Expires holds the timestamp when this entry expires. // Expires holds the timestamp when this entry expires.
// This does not mean that the entry may not be used anymore afterwards, // This does not mean that the entry may not be used anymore afterwards,
// but that this is used to calcuate the TTL of the database record. // but that this is used to calcuate the TTL of the database record.

View file

@ -49,9 +49,20 @@ type NameRecord struct {
Extra []string Extra []string
Expires int64 Expires int64
Server string Resolver *ResolverInfo
ServerScope int8 }
ServerInfo string
// IsValid returns whether the NameRecord is valid and may be used. Otherwise,
// it should be disregarded.
func (nameRecord *NameRecord) IsValid() bool {
switch {
case nameRecord.Resolver == nil || nameRecord.Resolver.Type == "":
// Changed in v0.6.7: Introduced Resolver *ResolverInfo
return false
default:
// Up to date!
return true
}
} }
func makeNameRecordKey(domain string, question string) string { func makeNameRecordKey(domain string, question string) string {
@ -67,7 +78,7 @@ func GetNameRecord(domain, question string) (*NameRecord, error) {
return nil, err return nil, err
} }
// unwrap // Unwrap record if it's wrapped.
if r.IsWrapped() { if r.IsWrapped() {
// only allocate a new struct, if we need it // only allocate a new struct, if we need it
new := &NameRecord{} new := &NameRecord{}
@ -75,14 +86,24 @@ func GetNameRecord(domain, question string) (*NameRecord, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Check if the record is valid.
if !new.IsValid() {
return nil, errors.New("record is invalid (outdated format)")
}
return new, nil return new, nil
} }
// or adjust type // Or just adjust the type.
new, ok := r.(*NameRecord) new, ok := r.(*NameRecord)
if !ok { if !ok {
return nil, fmt.Errorf("record not of type *NameRecord, but %T", r) return nil, fmt.Errorf("record not of type *NameRecord, but %T", r)
} }
// Check if the record is valid.
if !new.IsValid() {
return nil, errors.New("record is invalid (outdated format)")
}
return new, nil return new, nil
} }

View file

@ -175,9 +175,9 @@ func checkCache(ctx context.Context, q *Query) *RRCache {
} }
// Get the resolver that the rrCache was resolved with. // Get the resolver that the rrCache was resolved with.
resolver := getActiveResolverByIDWithLocking(rrCache.Server) resolver := getActiveResolverByIDWithLocking(rrCache.Resolver.ID())
if resolver == nil { if resolver == nil {
log.Tracer(ctx).Debugf("resolver: ignoring RRCache %s%s because source server %s has been removed", q.FQDN, q.QType.String(), rrCache.Server) log.Tracer(ctx).Debugf("resolver: ignoring RRCache %s%s because source server %q has been removed", q.FQDN, q.QType.String(), rrCache.Resolver.ID())
return nil return nil
} }
@ -361,11 +361,11 @@ resolveLoop:
continue continue
case errors.Is(err, ErrTimeout): case errors.Is(err, ErrTimeout):
resolver.Conn.ReportFailure() resolver.Conn.ReportFailure()
log.Tracer(ctx).Debugf("resolver: query to %s timed out", resolver.GetName()) log.Tracer(ctx).Debugf("resolver: query to %s timed out", resolver.Info.ID())
continue continue
default: default:
resolver.Conn.ReportFailure() resolver.Conn.ReportFailure()
log.Tracer(ctx).Debugf("resolver: query to %s failed: %s", resolver.GetName(), err) log.Tracer(ctx).Debugf("resolver: query to %s failed: %s", resolver.Info.ID(), err)
continue continue
} }
} }

View file

@ -20,12 +20,13 @@ const (
var ( var (
envResolver = &Resolver{ envResolver = &Resolver{
Server: ServerSourceEnv, ConfigURL: ServerSourceEnv,
ServerType: ServerTypeEnv, Info: &ResolverInfo{
ServerIPScope: netutils.SiteLocal, Type: ServerTypeEnv,
ServerInfo: "Portmaster environment", Source: ServerSourceEnv,
Source: ServerSourceEnv, IPScope: netutils.SiteLocal,
Conn: &envResolverConn{}, },
Conn: &envResolverConn{},
} }
envResolvers = []*Resolver{envResolver} envResolvers = []*Resolver{envResolver}
@ -109,14 +110,12 @@ func (er *envResolverConn) makeRRCache(q *Query, answers []dns.RR) *RRCache {
q.NoCaching = true q.NoCaching = true
return &RRCache{ return &RRCache{
Domain: q.FQDN, Domain: q.FQDN,
Question: q.QType, Question: q.QType,
RCode: dns.RcodeSuccess, RCode: dns.RcodeSuccess,
Answer: answers, Answer: answers,
Extra: []dns.RR{internalSpecialUseComment}, // Always add comment about this TLD. Extra: []dns.RR{internalSpecialUseComment}, // Always add comment about this TLD.
Server: envResolver.Server, Resolver: envResolver.Info.Copy(),
ServerScope: envResolver.ServerIPScope,
ServerInfo: envResolver.ServerInfo,
} }
} }

View file

@ -31,12 +31,13 @@ var (
questionsLock sync.Mutex questionsLock sync.Mutex
mDNSResolver = &Resolver{ mDNSResolver = &Resolver{
Server: ServerSourceMDNS, ConfigURL: ServerSourceMDNS,
ServerType: ServerTypeDNS, Info: &ResolverInfo{
ServerIPScope: netutils.SiteLocal, Type: ServerTypeMDNS,
ServerInfo: "mDNS resolver", Source: ServerSourceMDNS,
Source: ServerSourceMDNS, IPScope: netutils.SiteLocal,
Conn: &mDNSResolverConn{}, },
Conn: &mDNSResolverConn{},
} }
mDNSResolvers = []*Resolver{mDNSResolver} mDNSResolvers = []*Resolver{mDNSResolver}
) )
@ -200,12 +201,10 @@ func handleMDNSMessages(ctx context.Context, messages chan *dns.Msg) error {
// create new and do not append // create new and do not append
if err != nil || rrCache.Modified < time.Now().Add(-2*time.Second).Unix() || rrCache.Expired() { if err != nil || rrCache.Modified < time.Now().Add(-2*time.Second).Unix() || rrCache.Expired() {
rrCache = &RRCache{ rrCache = &RRCache{
Domain: question.Name, Domain: question.Name,
Question: dns.Type(question.Qtype), Question: dns.Type(question.Qtype),
RCode: dns.RcodeSuccess, RCode: dns.RcodeSuccess,
Server: mDNSResolver.Server, Resolver: mDNSResolver.Info.Copy(),
ServerScope: mDNSResolver.ServerIPScope,
ServerInfo: mDNSResolver.ServerInfo,
} }
} }
} }
@ -302,13 +301,11 @@ func handleMDNSMessages(ctx context.Context, messages chan *dns.Msg) error {
continue continue
} }
rrCache = &RRCache{ rrCache = &RRCache{
Domain: v.Header().Name, Domain: v.Header().Name,
Question: dns.Type(v.Header().Class), Question: dns.Type(v.Header().Class),
RCode: dns.RcodeSuccess, RCode: dns.RcodeSuccess,
Answer: []dns.RR{v}, Answer: []dns.RR{v},
Server: mDNSResolver.Server, Resolver: mDNSResolver.Info.Copy(),
ServerScope: mDNSResolver.ServerIPScope,
ServerInfo: mDNSResolver.ServerInfo,
} }
rrCache.Clean(minMDnsTTL) rrCache.Clean(minMDnsTTL)
err := rrCache.Save() err := rrCache.Save()
@ -423,12 +420,10 @@ func queryMulticastDNS(ctx context.Context, q *Query) (*RRCache, error) {
// Respond with NXDomain. // Respond with NXDomain.
return &RRCache{ return &RRCache{
Domain: q.FQDN, Domain: q.FQDN,
Question: q.QType, Question: q.QType,
RCode: dns.RcodeNameError, RCode: dns.RcodeNameError,
Server: mDNSResolver.Server, Resolver: mDNSResolver.Info.Copy(),
ServerScope: mDNSResolver.ServerIPScope,
ServerInfo: mDNSResolver.ServerInfo,
}, nil }, nil
} }

View file

@ -72,22 +72,20 @@ func (pr *PlainResolver) Query(ctx context.Context, q *Query) (*RRCache, error)
// check if blocked // check if blocked
if pr.resolver.IsBlockedUpstream(reply) { if pr.resolver.IsBlockedUpstream(reply) {
return nil, &BlockedUpstreamError{pr.resolver.GetName()} return nil, &BlockedUpstreamError{pr.resolver.Info.DescriptiveName()}
} }
// hint network environment at successful connection // hint network environment at successful connection
netenv.ReportSuccessfulConnection() netenv.ReportSuccessfulConnection()
newRecord := &RRCache{ newRecord := &RRCache{
Domain: q.FQDN, Domain: q.FQDN,
Question: q.QType, Question: q.QType,
RCode: reply.Rcode, RCode: reply.Rcode,
Answer: reply.Answer, Answer: reply.Answer,
Ns: reply.Ns, Ns: reply.Ns,
Extra: reply.Extra, Extra: reply.Extra,
Server: pr.resolver.Server, Resolver: pr.resolver.Info.Copy(),
ServerScope: pr.resolver.ServerIPScope,
ServerInfo: pr.resolver.ServerInfo,
} }
// TODO: check if reply.Answer is valid // TODO: check if reply.Answer is valid

View file

@ -49,15 +49,13 @@ type InFlightQuery struct {
// MakeCacheRecord creates an RCache record from a reply. // MakeCacheRecord creates an RCache record from a reply.
func (ifq *InFlightQuery) MakeCacheRecord(reply *dns.Msg) *RRCache { func (ifq *InFlightQuery) MakeCacheRecord(reply *dns.Msg) *RRCache {
return &RRCache{ return &RRCache{
Domain: ifq.Query.FQDN, Domain: ifq.Query.FQDN,
Question: ifq.Query.QType, Question: ifq.Query.QType,
RCode: reply.Rcode, RCode: reply.Rcode,
Answer: reply.Answer, Answer: reply.Answer,
Ns: reply.Ns, Ns: reply.Ns,
Extra: reply.Extra, Extra: reply.Extra,
Server: ifq.Resolver.Server, Resolver: ifq.Resolver.Info.Copy(),
ServerScope: ifq.Resolver.ServerIPScope,
ServerInfo: ifq.Resolver.ServerInfo,
} }
} }
@ -172,7 +170,7 @@ func (tr *TCPResolver) Query(ctx context.Context, q *Query) (*RRCache, error) {
} }
if tr.resolver.IsBlockedUpstream(reply) { if tr.resolver.IsBlockedUpstream(reply) {
return nil, &BlockedUpstreamError{tr.resolver.GetName()} return nil, &BlockedUpstreamError{tr.resolver.Info.DescriptiveName()}
} }
return inFlight.MakeCacheRecord(reply), nil return inFlight.MakeCacheRecord(reply), nil
@ -189,7 +187,7 @@ func (tr *TCPResolver) checkClientStatus() {
select { select {
case tr.clientHeartbeat <- struct{}{}: case tr.clientHeartbeat <- struct{}{}:
case <-time.After(heartbeatTimeout): case <-time.After(heartbeatTimeout):
log.Warningf("resolver: heartbeat failed for %s dns client, stopping", tr.resolver.GetName()) log.Warningf("resolver: heartbeat failed for %s dns client, stopping", tr.resolver.Info.DescriptiveName())
stopClient() stopClient()
} }
} }
@ -299,7 +297,7 @@ func (mgr *tcpResolverConnMgr) waitForWork(clientCtx context.Context) (proceed b
select { select {
case mgr.tr.queries <- inFlight.Msg: case mgr.tr.queries <- inFlight.Msg:
default: default:
log.Warningf("resolver: failed to re-inject abandoned query to %s", mgr.tr.resolver.GetName()) log.Warningf("resolver: failed to re-inject abandoned query to %s", mgr.tr.resolver.Info.DescriptiveName())
} }
} }
// in-flight queries that match the connection instance ID are not changed. They are already in the queue. // in-flight queries that match the connection instance ID are not changed. They are already in the queue.
@ -317,7 +315,7 @@ func (mgr *tcpResolverConnMgr) waitForWork(clientCtx context.Context) (proceed b
select { select {
case mgr.tr.queries <- msg: case mgr.tr.queries <- msg:
case <-time.After(2 * time.Second): case <-time.After(2 * time.Second):
log.Warningf("resolver: failed to re-inject waking query to %s", mgr.tr.resolver.GetName()) log.Warningf("resolver: failed to re-inject waking query to %s", mgr.tr.resolver.Info.DescriptiveName())
} }
return nil return nil
}) })
@ -343,7 +341,7 @@ func (mgr *tcpResolverConnMgr) establishConnection() (
var err error var err error
conn, err = mgr.tr.dnsClient.Dial(mgr.tr.resolver.ServerAddress) conn, err = mgr.tr.dnsClient.Dial(mgr.tr.resolver.ServerAddress)
if err != nil { if err != nil {
log.Debugf("resolver: failed to connect to %s (%s)", mgr.tr.resolver.GetName(), mgr.tr.resolver.ServerAddress) log.Debugf("resolver: failed to connect to %s", mgr.tr.resolver.Info.DescriptiveName())
return nil, nil, nil, nil return nil, nil, nil, nil
} }
connCtx, cancelConnCtx = context.WithCancel(context.Background()) connCtx, cancelConnCtx = context.WithCancel(context.Background())
@ -356,9 +354,8 @@ func (mgr *tcpResolverConnMgr) establishConnection() (
// Log that a connection to the resolver was established. // Log that a connection to the resolver was established.
log.Debugf( log.Debugf(
"resolver: connected to %s (%s) with %d queries waiting", "resolver: connected to %s with %d queries waiting",
mgr.tr.resolver.GetName(), mgr.tr.resolver.Info.DescriptiveName(),
conn.RemoteAddr(),
waitingQueries, waitingQueries,
) )
@ -434,7 +431,7 @@ func (mgr *tcpResolverConnMgr) queryHandler( //nolint:golint // context.Context
activeQueries := len(mgr.tr.inFlightQueries) activeQueries := len(mgr.tr.inFlightQueries)
mgr.tr.Unlock() mgr.tr.Unlock()
if activeQueries == 0 { if activeQueries == 0 {
log.Debugf("resolver: recycling conn to %s (%s)", mgr.tr.resolver.GetName(), conn.RemoteAddr()) log.Debugf("resolver: recycling conn to %s", mgr.tr.resolver.Info.DescriptiveName())
return true return true
} }
} }
@ -454,9 +451,8 @@ func (mgr *tcpResolverConnMgr) handleQueryResponse(conn *dns.Conn, msg *dns.Msg)
if !ok { if !ok {
log.Debugf( log.Debugf(
"resolver: received possibly unsolicited reply from %s (%s): txid=%d q=%+v", "resolver: received possibly unsolicited reply from %s: txid=%d q=%+v",
mgr.tr.resolver.GetName(), mgr.tr.resolver.Info.DescriptiveName(),
conn.RemoteAddr(),
msg.Id, msg.Id,
msg.Question, msg.Question,
) )
@ -519,24 +515,21 @@ func (mgr *tcpResolverConnMgr) logConnectionError(err error, conn *dns.Conn, con
switch { switch {
case errors.Is(err, io.EOF): case errors.Is(err, io.EOF):
log.Debugf( log.Debugf(
"resolver: connection to %s (%s) was closed with %d in-flight queries", "resolver: connection to %s was closed with %d in-flight queries",
mgr.tr.resolver.GetName(), mgr.tr.resolver.Info.DescriptiveName(),
conn.RemoteAddr(),
inFlightQueries, inFlightQueries,
) )
case reading: case reading:
log.Warningf( log.Warningf(
"resolver: read error from %s (%s) with %d in-flight queries: %s", "resolver: read error from %s with %d in-flight queries: %s",
mgr.tr.resolver.GetName(), mgr.tr.resolver.Info.DescriptiveName(),
conn.RemoteAddr(),
inFlightQueries, inFlightQueries,
err, err,
) )
default: default:
log.Warningf( log.Warningf(
"resolver: write error to %s (%s) with %d in-flight queries: %s", "resolver: write error to %s with %d in-flight queries: %s",
mgr.tr.resolver.GetName(), mgr.tr.resolver.Info.DescriptiveName(),
conn.RemoteAddr(),
inFlightQueries, inFlightQueries,
err, err,
) )

View file

@ -2,21 +2,24 @@ package resolver
import ( import (
"context" "context"
"fmt"
"net" "net"
"sync" "sync"
"time" "time"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/safing/portmaster/netenv" "github.com/safing/portmaster/netenv"
"github.com/safing/portmaster/network/netutils"
) )
// DNS Resolver Attributes // DNS Resolver Attributes
const ( const (
ServerTypeDNS = "dns" ServerTypeDNS = "dns"
ServerTypeTCP = "tcp" ServerTypeTCP = "tcp"
ServerTypeDoT = "dot" ServerTypeDoT = "dot"
ServerTypeDoH = "doh" ServerTypeDoH = "doh"
ServerTypeEnv = "env" ServerTypeMDNS = "mdns"
ServerTypeEnv = "env"
ServerSourceConfigured = "config" ServerSourceConfigured = "config"
ServerSourceOperatingSystem = "system" ServerSourceOperatingSystem = "system"
@ -39,14 +42,13 @@ type Resolver struct {
// - `empty`: NXDomain result, but without any other record in any section // - `empty`: NXDomain result, but without any other record in any section
// - `refused`: Request was refused // - `refused`: Request was refused
// - `zeroip`: Answer only contains zeroip // - `zeroip`: Answer only contains zeroip
Server string ConfigURL string
// Source describes from where the resolver configuration originated. // Info holds the parsed configuration.
Source string Info *ResolverInfo
// Name is the name of the resolver as passed via // ServerAddress holds the resolver address for easier use.
// ?name=. ServerAddress string
Name string
// UpstreamBlockDetection defines the detection type // UpstreamBlockDetection defines the detection type
// to identifier upstream DNS query blocking. // to identifier upstream DNS query blocking.
@ -57,14 +59,6 @@ type Resolver struct {
// - disabled // - disabled
UpstreamBlockDetection string UpstreamBlockDetection string
// Parsed config
ServerType string
ServerAddress string
ServerIP net.IP
ServerIPScope int8
ServerPort uint16
ServerInfo string
// Special Options // Special Options
VerifyDomain string VerifyDomain string
Search []string Search []string
@ -73,25 +67,111 @@ type Resolver struct {
Conn ResolverConn `json:"-"` Conn ResolverConn `json:"-"`
} }
// ResolverInfo is a subset of resolver attributes that is attached to answers
// from that server in order to use it later for decision making. It must not
// be changed by anyone after creation and initialization is complete.
type ResolverInfo struct {
// Name describes the name given to the resolver. The name is configured in the config URL using the name parameter.
Name string
// Type describes the type of the resolver.
// Possible values include dns, tcp, dot, doh, mdns, env.
Type string
// Source describes where the resolver configuration came from.
// Possible values include config, system, mdns, env.
Source string
// IP is the IP address of the resolver
IP net.IP
// IPScope is the network scope of the IP address.
IPScope netutils.IPScope
// Port is the udp/tcp port of the resolver.
Port uint16
// id holds a unique ID for this resolver.
id string
idGen sync.Once
}
// ID returns the unique ID of the resolver.
func (info *ResolverInfo) ID() string {
// Generate the ID the first time.
info.idGen.Do(func() {
switch info.Type {
case ServerTypeMDNS:
info.id = ServerTypeMDNS
case ServerTypeEnv:
info.id = ServerTypeEnv
default:
info.id = fmt.Sprintf(
"%s://%s:%d#%s",
info.Type,
info.IP,
info.Port,
info.Source,
)
}
})
return info.id
}
// DescriptiveName returns a human readable, but also detailed representation
// of the resolver.
func (info *ResolverInfo) DescriptiveName() string {
switch {
case info.Type == ServerTypeMDNS:
return "MDNS"
case info.Type == ServerTypeEnv:
return "Portmaster Environment"
case info.Name != "":
return fmt.Sprintf(
"%s (%s)",
info.Name,
info.ID(),
)
default:
return fmt.Sprintf(
"%s (%s)",
info.IP.String(),
info.ID(),
)
}
}
// Copy returns a full copy of the ResolverInfo.
func (info *ResolverInfo) Copy() *ResolverInfo {
// Force idGen to run before we copy.
_ = info.ID()
// Copy manually in order to not copy the mutex.
cp := &ResolverInfo{
Name: info.Name,
Type: info.Type,
Source: info.Source,
IP: info.IP,
IPScope: info.IPScope,
Port: info.Port,
id: info.id,
}
// Trigger idGen.Do(), as the ID is already generated.
cp.idGen.Do(func() {})
return cp
}
// IsBlockedUpstream returns true if the request has been blocked // IsBlockedUpstream returns true if the request has been blocked
// upstream. // upstream.
func (resolver *Resolver) IsBlockedUpstream(answer *dns.Msg) bool { func (resolver *Resolver) IsBlockedUpstream(answer *dns.Msg) bool {
return isBlockedUpstream(resolver, answer) return isBlockedUpstream(resolver, answer)
} }
// GetName returns the name of the server. If no name
// is configured the server address is returned.
func (resolver *Resolver) GetName() string {
if resolver.Name != "" {
return resolver.Name
}
return resolver.Server
}
// String returns the URL representation of the resolver. // String returns the URL representation of the resolver.
func (resolver *Resolver) String() string { func (resolver *Resolver) String() string {
return resolver.GetName() return resolver.Info.DescriptiveName()
} }
// ResolverConn is an interface to implement different types of query backends. // ResolverConn is an interface to implement different types of query backends.

View file

@ -52,7 +52,7 @@ func TestSingleResolving(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
t.Logf("running bulk query test with resolver %s", resolver.Server) t.Logf("running bulk query test with resolver %s", resolver.Info.DescriptiveName())
started := time.Now() started := time.Now()
@ -83,7 +83,7 @@ func TestBulkResolving(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
t.Logf("running bulk query test with resolver %s", resolver.Server) t.Logf("running bulk query test with resolver %s", resolver.Info.DescriptiveName())
started := time.Now() started := time.Now()

View file

@ -5,6 +5,7 @@ import (
"net" "net"
"net/url" "net/url"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
@ -61,7 +62,7 @@ func formatIPAndPort(ip net.IP, port uint16) string {
} }
func resolverConnFactory(resolver *Resolver) ResolverConn { func resolverConnFactory(resolver *Resolver) ResolverConn {
switch resolver.ServerType { switch resolver.Info.Type {
case ServerTypeTCP: case ServerTypeTCP:
return NewTCPResolver(resolver) return NewTCPResolver(resolver)
case ServerTypeDoT: case ServerTypeDoT:
@ -82,26 +83,36 @@ func createResolver(resolverURL, source string) (*Resolver, bool, error) {
switch u.Scheme { switch u.Scheme {
case ServerTypeDNS, ServerTypeDoT, ServerTypeTCP: case ServerTypeDNS, ServerTypeDoT, ServerTypeTCP:
default: default:
return nil, false, fmt.Errorf("invalid DNS resolver scheme %q", u.Scheme) return nil, false, fmt.Errorf("DNS resolver scheme %q invalid", u.Scheme)
} }
ip := net.ParseIP(u.Hostname()) ip := net.ParseIP(u.Hostname())
if ip == nil { if ip == nil {
return nil, false, fmt.Errorf("invalid resolver IP") return nil, false, fmt.Errorf("resolver IP %q invalid", u.Hostname())
} }
// Add default port for scheme if it is missing. // Add default port for scheme if it is missing.
if u.Port() == "" { var port uint16
switch u.Scheme { hostPort := u.Port()
case ServerTypeDNS, ServerTypeTCP: switch {
u.Host += ":53" case hostPort != "":
case ServerTypeDoT: parsedPort, err := strconv.ParseUint(hostPort, 10, 16)
u.Host += ":853" if err != nil {
return nil, false, fmt.Errorf("resolver port %q invalid", u.Port())
} }
port = uint16(parsedPort)
case u.Scheme == ServerTypeDNS, u.Scheme == ServerTypeTCP:
port = 53
case u.Scheme == ServerTypeDoH:
port = 443
case u.Scheme == ServerTypeDoT:
port = 853
default:
return nil, false, fmt.Errorf("missing port in %q", u.Host)
} }
scope := netutils.ClassifyIP(ip) scope := netutils.GetIPScope(ip)
if scope == netutils.HostLocal { if scope.IsLocalhost() {
return nil, true, nil // skip return nil, true, nil // skip
} }
@ -127,24 +138,20 @@ func createResolver(resolverURL, source string) (*Resolver, bool, error) {
} }
new := &Resolver{ new := &Resolver{
Server: resolverURL, ConfigURL: resolverURL,
ServerType: u.Scheme, Info: &ResolverInfo{
ServerAddress: u.Host, Name: query.Get("name"),
ServerIP: ip, Type: u.Scheme,
ServerIPScope: scope, Source: source,
Source: source, IP: ip,
IPScope: scope,
Port: port,
},
ServerAddress: net.JoinHostPort(ip.String(), strconv.Itoa(int(port))),
VerifyDomain: verifyDomain, VerifyDomain: verifyDomain,
Name: query.Get("name"),
UpstreamBlockDetection: blockType, UpstreamBlockDetection: blockType,
} }
u.RawQuery = "" // Remove options from parsed URL
if new.Name != "" {
new.ServerInfo = fmt.Sprintf("%s (%s, from %s)", new.Name, u, source)
} else {
new.ServerInfo = fmt.Sprintf("%s (from %s)", u, source)
}
new.Conn = resolverConnFactory(new) new.Conn = resolverConnFactory(new)
return new, false, nil return new, false, nil
} }
@ -195,7 +202,7 @@ func getSystemResolvers() (resolvers []*Resolver) {
continue continue
} }
if netutils.IPIsLAN(nameserver.IP) { if resolver.Info.IPScope.IsLAN() {
configureSearchDomains(resolver, nameserver.Search) configureSearchDomains(resolver, nameserver.Search)
} }
@ -244,16 +251,16 @@ func loadResolvers() {
activeResolvers = make(map[string]*Resolver) activeResolvers = make(map[string]*Resolver)
// add // add
for _, resolver := range newResolvers { for _, resolver := range newResolvers {
activeResolvers[resolver.Server] = resolver activeResolvers[resolver.Info.ID()] = resolver
} }
activeResolvers[mDNSResolver.Server] = mDNSResolver activeResolvers[mDNSResolver.Info.ID()] = mDNSResolver
activeResolvers[envResolver.Server] = envResolver activeResolvers[envResolver.Info.ID()] = envResolver
// log global resolvers // log global resolvers
if len(globalResolvers) > 0 { if len(globalResolvers) > 0 {
log.Trace("resolver: loaded global resolvers:") log.Trace("resolver: loaded global resolvers:")
for _, resolver := range globalResolvers { for _, resolver := range globalResolvers {
log.Tracef("resolver: %s", resolver.Server) log.Tracef("resolver: %s", resolver.ConfigURL)
} }
} else { } else {
log.Warning("resolver: no global resolvers loaded") log.Warning("resolver: no global resolvers loaded")
@ -263,7 +270,7 @@ func loadResolvers() {
if len(localResolvers) > 0 { if len(localResolvers) > 0 {
log.Trace("resolver: loaded local resolvers:") log.Trace("resolver: loaded local resolvers:")
for _, resolver := range localResolvers { for _, resolver := range localResolvers {
log.Tracef("resolver: %s", resolver.Server) log.Tracef("resolver: %s", resolver.ConfigURL)
} }
} else { } else {
log.Info("resolver: no local resolvers loaded") log.Info("resolver: no local resolvers loaded")
@ -273,7 +280,7 @@ func loadResolvers() {
if len(systemResolvers) > 0 { if len(systemResolvers) > 0 {
log.Trace("resolver: loaded system/network-assigned resolvers:") log.Trace("resolver: loaded system/network-assigned resolvers:")
for _, resolver := range systemResolvers { for _, resolver := range systemResolvers {
log.Tracef("resolver: %s", resolver.Server) log.Tracef("resolver: %s", resolver.ConfigURL)
} }
} else { } else {
log.Info("resolver: no system/network-assigned resolvers loaded") log.Info("resolver: no system/network-assigned resolvers loaded")
@ -285,7 +292,7 @@ func loadResolvers() {
for _, scope := range localScopes { for _, scope := range localScopes {
var scopeServers []string var scopeServers []string
for _, resolver := range scope.Resolvers { for _, resolver := range scope.Resolvers {
scopeServers = append(scopeServers, resolver.Server) scopeServers = append(scopeServers, resolver.ConfigURL)
} }
log.Tracef("resolver: %s: %s", scope.Domain, strings.Join(scopeServers, ", ")) log.Tracef("resolver: %s: %s", scope.Domain, strings.Join(scopeServers, ", "))
} }
@ -306,11 +313,11 @@ func setScopedResolvers(resolvers []*Resolver) {
localScopes = make([]*Scope, 0) localScopes = make([]*Scope, 0)
for _, resolver := range resolvers { for _, resolver := range resolvers {
if resolver.ServerIP != nil && netutils.IPIsLAN(resolver.ServerIP) { if resolver.Info.IPScope.IsLAN() {
localResolvers = append(localResolvers, resolver) localResolvers = append(localResolvers, resolver)
} }
if resolver.Source == ServerSourceOperatingSystem { if resolver.Info.Source == ServerSourceOperatingSystem {
systemResolvers = append(systemResolvers, resolver) systemResolvers = append(systemResolvers, resolver)
} }

View file

@ -29,10 +29,8 @@ type RRCache struct {
Extra []dns.RR Extra []dns.RR
Expires int64 Expires int64
// Source Information // Resolver Information
Server string Resolver *ResolverInfo
ServerScope int8
ServerInfo string
// Metadata about the request and handling // Metadata about the request and handling
ServedFromCache bool ServedFromCache bool
@ -133,13 +131,11 @@ func (rrCache *RRCache) ExportAllARecords() (ips []net.IP) {
// ToNameRecord converts the RRCache to a NameRecord for cleaner persistence. // ToNameRecord converts the RRCache to a NameRecord for cleaner persistence.
func (rrCache *RRCache) ToNameRecord() *NameRecord { func (rrCache *RRCache) ToNameRecord() *NameRecord {
new := &NameRecord{ new := &NameRecord{
Domain: rrCache.Domain, Domain: rrCache.Domain,
Question: rrCache.Question.String(), Question: rrCache.Question.String(),
RCode: rrCache.RCode, RCode: rrCache.RCode,
Expires: rrCache.Expires, Expires: rrCache.Expires,
Server: rrCache.Server, Resolver: rrCache.Resolver,
ServerScope: rrCache.ServerScope,
ServerInfo: rrCache.ServerInfo,
} }
// stringify RR entries // stringify RR entries
@ -204,9 +200,7 @@ func GetRRCache(domain string, question dns.Type) (*RRCache, error) {
rrCache.Extra = parseRR(rrCache.Extra, entry) rrCache.Extra = parseRR(rrCache.Extra, entry)
} }
rrCache.Server = nameRecord.Server rrCache.Resolver = nameRecord.Resolver
rrCache.ServerScope = nameRecord.ServerScope
rrCache.ServerInfo = nameRecord.ServerInfo
rrCache.ServedFromCache = true rrCache.ServedFromCache = true
rrCache.Modified = nameRecord.Meta().Modified rrCache.Modified = nameRecord.Meta().Modified
return rrCache, nil return rrCache, nil
@ -259,9 +253,7 @@ func (rrCache *RRCache) ShallowCopy() *RRCache {
Extra: rrCache.Extra, Extra: rrCache.Extra,
Expires: rrCache.Expires, Expires: rrCache.Expires,
Server: rrCache.Server, Resolver: rrCache.Resolver,
ServerScope: rrCache.ServerScope,
ServerInfo: rrCache.ServerInfo,
ServedFromCache: rrCache.ServedFromCache, ServedFromCache: rrCache.ServedFromCache,
RequestingNew: rrCache.RequestingNew, RequestingNew: rrCache.RequestingNew,
@ -302,9 +294,9 @@ func (rrCache *RRCache) ReplyWithDNS(ctx context.Context, request *dns.Msg) *dns
func (rrCache *RRCache) GetExtraRRs(ctx context.Context, query *dns.Msg) (extra []dns.RR) { func (rrCache *RRCache) GetExtraRRs(ctx context.Context, query *dns.Msg) (extra []dns.RR) {
// Add cache status and source of data. // Add cache status and source of data.
if rrCache.ServedFromCache { if rrCache.ServedFromCache {
extra = addExtra(ctx, extra, "served from cache, resolved by "+rrCache.ServerInfo) extra = addExtra(ctx, extra, "served from cache, resolved by "+rrCache.Resolver.DescriptiveName())
} else { } else {
extra = addExtra(ctx, extra, "freshly resolved by "+rrCache.ServerInfo) extra = addExtra(ctx, extra, "freshly resolved by "+rrCache.Resolver.DescriptiveName())
} }
// Add expiry and cache information. // Add expiry and cache information.

View file

@ -158,13 +158,13 @@ addNextResolver:
for _, resolver := range addResolvers { for _, resolver := range addResolvers {
// check for compliance // check for compliance
if err := resolver.checkCompliance(ctx, q); err != nil { if err := resolver.checkCompliance(ctx, q); err != nil {
log.Tracer(ctx).Tracef("skipping non-compliant resolver %s: %s", resolver.GetName(), err) log.Tracer(ctx).Tracef("skipping non-compliant resolver %s: %s", resolver.Info.DescriptiveName(), err)
continue continue
} }
// deduplicate // deduplicate
for _, selectedResolver := range selected { for _, selectedResolver := range selected {
if selectedResolver.Server == resolver.Server { if selectedResolver.Info.ID() == resolver.Info.ID() {
continue addNextResolver continue addNextResolver
} }
} }
@ -208,7 +208,7 @@ func (q *Query) checkCompliance() error {
func (resolver *Resolver) checkCompliance(_ context.Context, q *Query) error { func (resolver *Resolver) checkCompliance(_ context.Context, q *Query) error {
if noInsecureProtocols(q.SecurityLevel) { if noInsecureProtocols(q.SecurityLevel) {
switch resolver.ServerType { switch resolver.Info.Type {
case ServerTypeDNS: case ServerTypeDNS:
return errInsecureProtocol return errInsecureProtocol
case ServerTypeTCP: case ServerTypeTCP:
@ -218,20 +218,20 @@ func (resolver *Resolver) checkCompliance(_ context.Context, q *Query) error {
case ServerTypeDoH: case ServerTypeDoH:
// compliant // compliant
case ServerTypeEnv: case ServerTypeEnv:
// compliant (data is sources from local network only and is highly limited) // compliant (data is sourced from local network only and is highly limited)
default: default:
return errInsecureProtocol return errInsecureProtocol
} }
} }
if noAssignedNameservers(q.SecurityLevel) { if noAssignedNameservers(q.SecurityLevel) {
if resolver.Source == ServerSourceOperatingSystem { if resolver.Info.Source == ServerSourceOperatingSystem {
return errAssignedServer return errAssignedServer
} }
} }
if noMulticastDNS(q.SecurityLevel) { if noMulticastDNS(q.SecurityLevel) {
if resolver.Source == ServerSourceMDNS { if resolver.Info.Source == ServerSourceMDNS {
return errMulticastDNS return errMulticastDNS
} }
} }