wip: migrate to mono-repo. SPN has already been moved to spn/
This commit is contained in:
348
service/firewall/dns.go
Normal file
348
service/firewall/dns.go
Normal file
@@ -0,0 +1,348 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/service/network"
|
||||
"github.com/safing/portmaster/service/network/netutils"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
"github.com/safing/portmaster/service/profile/endpoints"
|
||||
"github.com/safing/portmaster/service/resolver"
|
||||
)
|
||||
|
||||
func filterDNSSection(
|
||||
ctx context.Context,
|
||||
entries []dns.RR,
|
||||
p *profile.LayeredProfile,
|
||||
resolverScope netutils.IPScope,
|
||||
sysResolver bool,
|
||||
) ([]dns.RR, []string, int, string) {
|
||||
// Will be filled 1:1 most of the time.
|
||||
goodEntries := make([]dns.RR, 0, len(entries))
|
||||
|
||||
// Will stay empty most of the time.
|
||||
var filteredRecords []string
|
||||
|
||||
// keeps track of the number of valid and allowed
|
||||
// A and AAAA records.
|
||||
var allowedAddressRecords int
|
||||
var interveningOptionKey string
|
||||
|
||||
for _, rr := range entries {
|
||||
// get IP and classification
|
||||
var ip net.IP
|
||||
switch v := rr.(type) {
|
||||
case *dns.A:
|
||||
ip = v.A
|
||||
case *dns.AAAA:
|
||||
ip = v.AAAA
|
||||
default:
|
||||
// add non A/AAAA entries
|
||||
// TODO: Add support for dns.SVCB and dns.HTTPS
|
||||
goodEntries = append(goodEntries, rr)
|
||||
continue
|
||||
}
|
||||
ipScope := netutils.GetIPScope(ip)
|
||||
|
||||
if p.RemoveOutOfScopeDNS() {
|
||||
switch {
|
||||
case ipScope.IsLocalhost():
|
||||
// No DNS should return localhost addresses
|
||||
filteredRecords = append(filteredRecords, formatRR(rr))
|
||||
interveningOptionKey = profile.CfgOptionRemoveOutOfScopeDNSKey
|
||||
log.Tracer(ctx).Tracef("filter: RR violates resolver scope: %s", formatRR(rr))
|
||||
continue
|
||||
|
||||
case resolverScope.IsGlobal() && ipScope.IsLAN() && !sysResolver:
|
||||
// No global DNS should return LAN addresses
|
||||
filteredRecords = append(filteredRecords, formatRR(rr))
|
||||
interveningOptionKey = profile.CfgOptionRemoveOutOfScopeDNSKey
|
||||
log.Tracer(ctx).Tracef("filter: RR violates resolver scope: %s", formatRR(rr))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if p.RemoveBlockedDNS() && !sysResolver {
|
||||
// filter by flags
|
||||
switch {
|
||||
case p.BlockScopeInternet() && ipScope.IsGlobal():
|
||||
filteredRecords = append(filteredRecords, formatRR(rr))
|
||||
interveningOptionKey = profile.CfgOptionBlockScopeInternetKey
|
||||
log.Tracer(ctx).Tracef("filter: RR is in blocked scope Internet: %s", formatRR(rr))
|
||||
continue
|
||||
|
||||
case p.BlockScopeLAN() && ipScope.IsLAN():
|
||||
filteredRecords = append(filteredRecords, formatRR(rr))
|
||||
interveningOptionKey = profile.CfgOptionBlockScopeLANKey
|
||||
log.Tracer(ctx).Tracef("filter: RR is in blocked scope LAN: %s", formatRR(rr))
|
||||
continue
|
||||
|
||||
case p.BlockScopeLocal() && ipScope.IsLocalhost():
|
||||
filteredRecords = append(filteredRecords, formatRR(rr))
|
||||
interveningOptionKey = profile.CfgOptionBlockScopeLocalKey
|
||||
log.Tracer(ctx).Tracef("filter: RR is in blocked scope Localhost: %s", formatRR(rr))
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: filter by endpoint list (IP only)
|
||||
}
|
||||
|
||||
// if survived, add to good entries
|
||||
allowedAddressRecords++
|
||||
goodEntries = append(goodEntries, rr)
|
||||
}
|
||||
|
||||
return goodEntries, filteredRecords, allowedAddressRecords, interveningOptionKey
|
||||
}
|
||||
|
||||
func filterDNSResponse(
|
||||
ctx context.Context,
|
||||
conn *network.Connection,
|
||||
p *profile.LayeredProfile,
|
||||
rrCache *resolver.RRCache,
|
||||
sysResolver bool,
|
||||
) *resolver.RRCache {
|
||||
// do not modify own queries
|
||||
if conn.Process().Pid == ownPID {
|
||||
return rrCache
|
||||
}
|
||||
|
||||
// check if DNS response filtering is completely turned off
|
||||
if !p.RemoveOutOfScopeDNS() && !p.RemoveBlockedDNS() {
|
||||
return rrCache
|
||||
}
|
||||
|
||||
var filteredRecords []string
|
||||
var validIPs int
|
||||
var interveningOptionKey string
|
||||
|
||||
rrCache.Answer, filteredRecords, validIPs, interveningOptionKey = filterDNSSection(ctx, rrCache.Answer, p, rrCache.Resolver.IPScope, sysResolver)
|
||||
if len(filteredRecords) > 0 {
|
||||
rrCache.FilteredEntries = append(rrCache.FilteredEntries, filteredRecords...)
|
||||
}
|
||||
|
||||
// Don't count the valid IPs in the extra section.
|
||||
rrCache.Extra, filteredRecords, _, _ = filterDNSSection(ctx, rrCache.Extra, p, rrCache.Resolver.IPScope, sysResolver)
|
||||
if len(filteredRecords) > 0 {
|
||||
rrCache.FilteredEntries = append(rrCache.FilteredEntries, filteredRecords...)
|
||||
}
|
||||
|
||||
if len(rrCache.FilteredEntries) > 0 {
|
||||
rrCache.Filtered = true
|
||||
if validIPs == 0 {
|
||||
switch interveningOptionKey {
|
||||
case profile.CfgOptionBlockScopeInternetKey:
|
||||
conn.Block("Internet access blocked", interveningOptionKey)
|
||||
case profile.CfgOptionBlockScopeLANKey:
|
||||
conn.Block("LAN access blocked", interveningOptionKey)
|
||||
case profile.CfgOptionBlockScopeLocalKey:
|
||||
conn.Block("Localhost access blocked", interveningOptionKey)
|
||||
case profile.CfgOptionRemoveOutOfScopeDNSKey:
|
||||
conn.Block("DNS global/private split-view violation", interveningOptionKey)
|
||||
default:
|
||||
conn.Block("DNS response only contained to-be-blocked IPs", interveningOptionKey)
|
||||
}
|
||||
|
||||
return rrCache
|
||||
}
|
||||
}
|
||||
|
||||
return rrCache
|
||||
}
|
||||
|
||||
// FilterResolvedDNS filters a dns response according to the application
|
||||
// profile and settings.
|
||||
func FilterResolvedDNS(
|
||||
ctx context.Context,
|
||||
conn *network.Connection,
|
||||
q *resolver.Query,
|
||||
rrCache *resolver.RRCache,
|
||||
) *resolver.RRCache {
|
||||
// Check if we have a process and profile.
|
||||
layeredProfile := conn.Process().Profile()
|
||||
if layeredProfile == nil {
|
||||
log.Tracer(ctx).Warning("unknown process or profile")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Don't filter env responses.
|
||||
if rrCache.Resolver.Type == resolver.ServerTypeEnv {
|
||||
return rrCache
|
||||
}
|
||||
|
||||
// special grant for connectivity domains
|
||||
if checkConnectivityDomain(ctx, conn, layeredProfile, nil) {
|
||||
// returns true if check triggered
|
||||
return rrCache
|
||||
}
|
||||
|
||||
// Only filter critical things if request comes from the system resolver.
|
||||
sysResolver := conn.Process().IsSystemResolver()
|
||||
|
||||
// Filter dns records and return if the query is blocked.
|
||||
rrCache = filterDNSResponse(ctx, conn, layeredProfile, rrCache, sysResolver)
|
||||
if conn.Verdict == network.VerdictBlock {
|
||||
return rrCache
|
||||
}
|
||||
|
||||
// Block by CNAMEs.
|
||||
if !sysResolver {
|
||||
mayBlockCNAMEs(ctx, conn, layeredProfile)
|
||||
}
|
||||
|
||||
return rrCache
|
||||
}
|
||||
|
||||
func mayBlockCNAMEs(ctx context.Context, conn *network.Connection, p *profile.LayeredProfile) bool {
|
||||
// if we have CNAMEs and the profile is configured to filter them
|
||||
// we need to re-check the lists and endpoints here
|
||||
if p.FilterCNAMEs() {
|
||||
conn.Entity.ResetLists()
|
||||
conn.Entity.EnableCNAMECheck(ctx, true)
|
||||
|
||||
result, reason := p.MatchEndpoint(ctx, conn.Entity)
|
||||
if result == endpoints.Denied {
|
||||
conn.BlockWithContext(reason.String(), profile.CfgOptionFilterCNAMEKey, reason.Context())
|
||||
return true
|
||||
}
|
||||
|
||||
if result == endpoints.NoMatch {
|
||||
result, reason = p.MatchFilterLists(ctx, conn.Entity)
|
||||
if result == endpoints.Denied {
|
||||
conn.BlockWithContext(reason.String(), profile.CfgOptionFilterCNAMEKey, reason.Context())
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// UpdateIPsAndCNAMEs saves all the IP->Name mappings to the cache database and
|
||||
// updates the CNAMEs in the Connection's Entity.
|
||||
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.
|
||||
var profileID string
|
||||
localProfile := conn.Process().Profile().LocalProfile()
|
||||
switch localProfile.ID {
|
||||
case profile.UnidentifiedProfileID,
|
||||
profile.SystemResolverProfileID:
|
||||
profileID = resolver.IPInfoProfileScopeGlobal
|
||||
default:
|
||||
profileID = localProfile.ID
|
||||
}
|
||||
|
||||
// Collect IPs and CNAMEs.
|
||||
cnames := make(map[string]string)
|
||||
ips := make([]net.IP, 0, len(rrCache.Answer))
|
||||
|
||||
for _, rr := range append(rrCache.Answer, rrCache.Extra...) {
|
||||
switch v := rr.(type) {
|
||||
case *dns.CNAME:
|
||||
cnames[v.Hdr.Name] = v.Target
|
||||
|
||||
case *dns.A:
|
||||
ips = append(ips, v.A)
|
||||
|
||||
case *dns.AAAA:
|
||||
ips = append(ips, v.AAAA)
|
||||
|
||||
case *dns.SVCB:
|
||||
if len(v.Target) >= 2 { // Ignore "" and ".".
|
||||
cnames[v.Hdr.Name] = v.Target
|
||||
}
|
||||
for _, pair := range v.Value {
|
||||
switch svcbParam := pair.(type) {
|
||||
case *dns.SVCBIPv4Hint:
|
||||
ips = append(ips, svcbParam.Hint...)
|
||||
case *dns.SVCBIPv6Hint:
|
||||
ips = append(ips, svcbParam.Hint...)
|
||||
}
|
||||
}
|
||||
|
||||
case *dns.HTTPS:
|
||||
if len(v.Target) >= 2 { // Ignore "" and ".".
|
||||
cnames[v.Hdr.Name] = v.Target
|
||||
}
|
||||
for _, pair := range v.Value {
|
||||
switch svcbParam := pair.(type) {
|
||||
case *dns.SVCBIPv4Hint:
|
||||
ips = append(ips, svcbParam.Hint...)
|
||||
case *dns.SVCBIPv6Hint:
|
||||
ips = append(ips, svcbParam.Hint...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Package IPs and CNAMEs into IPInfo structs.
|
||||
for _, ip := range ips {
|
||||
// Never save domain attributions for localhost IPs.
|
||||
if netutils.GetIPScope(ip) == netutils.HostLocal {
|
||||
continue
|
||||
}
|
||||
|
||||
// Create new record for this IP.
|
||||
record := resolver.ResolvedDomain{
|
||||
Domain: q.FQDN,
|
||||
Resolver: rrCache.Resolver,
|
||||
DNSRequestContext: rrCache.ToDNSRequestContext(),
|
||||
Expires: rrCache.Expires,
|
||||
}
|
||||
|
||||
// Resolve all CNAMEs in the correct order and add the to the record.
|
||||
domain := q.FQDN
|
||||
for {
|
||||
nextDomain, isCNAME := cnames[domain]
|
||||
if !isCNAME {
|
||||
break
|
||||
}
|
||||
|
||||
record.CNAMEs = append(record.CNAMEs, nextDomain)
|
||||
domain = nextDomain
|
||||
}
|
||||
|
||||
// Update the entity to include the CNAMEs of the query response.
|
||||
conn.Entity.CNAME = record.CNAMEs
|
||||
|
||||
// Check if there is an existing record for this DNS response.
|
||||
// Else create a new one.
|
||||
ipString := ip.String()
|
||||
info, err := resolver.GetIPInfo(profileID, ipString)
|
||||
if err != nil {
|
||||
if !errors.Is(err, database.ErrNotFound) {
|
||||
log.Errorf("nameserver: failed to search for IP info record: %s", err)
|
||||
}
|
||||
|
||||
info = &resolver.IPInfo{
|
||||
IP: ipString,
|
||||
ProfileID: profileID,
|
||||
}
|
||||
}
|
||||
|
||||
// Add the new record to the resolved domains for this IP and scope.
|
||||
info.AddDomain(record)
|
||||
|
||||
// Save if the record is new or has been updated.
|
||||
if err := info.Save(); err != nil {
|
||||
log.Errorf("nameserver: failed to save IP info record: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// formatRR is a friendlier alternative to miekg/dns.RR.String().
|
||||
func formatRR(rr dns.RR) string {
|
||||
return strings.ReplaceAll(rr.String(), "\t", " ")
|
||||
}
|
||||
Reference in New Issue
Block a user