mirror of
https://github.com/safing/portmaster
synced 2025-04-23 04:19:10 +00:00
348 lines
9.8 KiB
Go
348 lines
9.8 KiB
Go
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", " ")
|
|
}
|