Merge pull request #293 from safing/fix/patch-set-3

DNS and other fixes & improvements
This commit is contained in:
Daniel 2021-04-19 15:16:18 +02:00 committed by GitHub
commit 06eee6805c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 309 additions and 233 deletions

View file

@ -4,7 +4,6 @@ import (
"context"
"net"
"strings"
"time"
"github.com/miekg/dns"
"github.com/safing/portbase/database"
@ -16,9 +15,19 @@ import (
"github.com/safing/portmaster/resolver"
)
func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, resolverScope netutils.IPScope, sysResolver bool) ([]dns.RR, []string, int, string) {
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))
filteredRecords := make([]string, 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.
@ -44,13 +53,16 @@ func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, resolverScope
switch {
case ipScope.IsLocalhost():
// No DNS should return localhost addresses
filteredRecords = append(filteredRecords, rr.String())
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, rr.String())
filteredRecords = append(filteredRecords, formatRR(rr))
interveningOptionKey = profile.CfgOptionRemoveOutOfScopeDNSKey
log.Tracer(ctx).Tracef("filter: RR violates resolver scope: %s", formatRR(rr))
continue
}
}
@ -59,16 +71,21 @@ func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, resolverScope
// filter by flags
switch {
case p.BlockScopeInternet() && ipScope.IsGlobal():
filteredRecords = append(filteredRecords, rr.String())
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, rr.String())
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, rr.String())
filteredRecords = append(filteredRecords, formatRR(rr))
interveningOptionKey = profile.CfgOptionBlockScopeLocalKey
log.Tracer(ctx).Tracef("filter: RR is in blocked scope Localhost: %s", formatRR(rr))
continue
}
@ -83,9 +100,13 @@ func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, resolverScope
return goodEntries, filteredRecords, allowedAddressRecords, interveningOptionKey
}
func filterDNSResponse(conn *network.Connection, rrCache *resolver.RRCache, sysResolver bool) *resolver.RRCache {
p := conn.Process().Profile()
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
@ -96,20 +117,20 @@ func filterDNSResponse(conn *network.Connection, rrCache *resolver.RRCache, sysR
return rrCache
}
// duplicate entry
rrCache = rrCache.ShallowCopy()
rrCache.FilteredEntries = make([]string, 0)
var filteredRecords []string
var validIPs int
var interveningOptionKey string
rrCache.Answer, filteredRecords, validIPs, interveningOptionKey = filterDNSSection(rrCache.Answer, p, rrCache.Resolver.IPScope, sysResolver)
rrCache.Answer, filteredRecords, validIPs, interveningOptionKey = filterDNSSection(ctx, rrCache.Answer, p, rrCache.Resolver.IPScope, sysResolver)
if len(filteredRecords) > 0 {
rrCache.FilteredEntries = append(rrCache.FilteredEntries, filteredRecords...)
}
// we don't count the valid IPs in the extra section
rrCache.Extra, filteredRecords, _, _ = filterDNSSection(rrCache.Extra, p, rrCache.Resolver.IPScope, sysResolver)
// 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
@ -127,34 +148,8 @@ func filterDNSResponse(conn *network.Connection, rrCache *resolver.RRCache, sysR
conn.Block("DNS response only contained to-be-blocked IPs", interveningOptionKey)
}
// If all entries are filtered, this could mean that these are broken/bogus resource records.
if rrCache.Expired() {
// If the entry is expired, force delete it.
err := resolver.ResetCachedRecord(rrCache.Domain, rrCache.Question.String())
if err != nil && err != database.ErrNotFound {
log.Warningf(
"filter: failed to delete fully filtered name cache for %s: %s",
rrCache.ID(),
err,
)
return rrCache
}
} else if rrCache.Expires > time.Now().Add(10*time.Second).Unix() {
// Set a low TTL of 10 seconds if TTL is higher than that.
rrCache.Expires = time.Now().Add(10 * time.Second).Unix()
err := rrCache.Save()
if err != nil {
log.Debugf(
"filter: failed to set shorter TTL on fully filtered name cache for %s: %s",
rrCache.ID(),
err,
)
}
}
return nil
}
log.Infof("filter: filtered DNS replies for %s: %s", conn, strings.Join(rrCache.FilteredEntries, ", "))
}
return rrCache
@ -168,9 +163,15 @@ func FilterResolvedDNS(
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
}
// special grant for connectivity domains
if checkConnectivityDomain(ctx, conn, nil) {
if checkConnectivityDomain(ctx, conn, layeredProfile, nil) {
// returns true if check triggered
return rrCache
}
@ -178,33 +179,35 @@ func FilterResolvedDNS(
// Only filter criticial things if request comes from the system resolver.
sysResolver := conn.Process().IsSystemResolver()
updatedRR := filterDNSResponse(conn, rrCache, sysResolver)
if updatedRR == nil {
return nil
// Filter dns records and return if the query is blocked.
rrCache = filterDNSResponse(ctx, conn, layeredProfile, rrCache, sysResolver)
if conn.Verdict == network.VerdictBlock {
return rrCache
}
if !sysResolver && mayBlockCNAMEs(ctx, conn) {
return nil
// Block by CNAMEs.
if !sysResolver {
mayBlockCNAMEs(ctx, conn, layeredProfile)
}
return updatedRR
return rrCache
}
func mayBlockCNAMEs(ctx context.Context, conn *network.Connection) bool {
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 conn.Process().Profile().FilterCNAMEs() {
if p.FilterCNAMEs() {
conn.Entity.ResetLists()
conn.Entity.EnableCNAMECheck(ctx, true)
result, reason := conn.Process().Profile().MatchEndpoint(ctx, conn.Entity)
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 = conn.Process().Profile().MatchFilterLists(ctx, conn.Entity)
result, reason = p.MatchFilterLists(ctx, conn.Entity)
if result == endpoints.Denied {
conn.BlockWithContext(reason.String(), profile.CfgOptionFilterCNAMEKey, reason.Context())
return true
@ -304,3 +307,8 @@ func UpdateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *netw
}
}
}
// formatRR is a friendlier alternative to miekg/dns.RR.String().
func formatRR(rr dns.RR) string {
return strings.ReplaceAll(rr.String(), "\t", " ")
}

View file

@ -144,15 +144,14 @@ func getConnection(pkt packet.Packet) (*network.Connection, error) {
// Transform and log result.
conn := newConn.(*network.Connection)
switch {
case created && shared:
log.Tracer(pkt.Ctx()).Tracef("filter: created new connection %s (shared)", conn.ID)
case created:
log.Tracer(pkt.Ctx()).Tracef("filter: created new connection %s", conn.ID)
case shared:
log.Tracer(pkt.Ctx()).Tracef("filter: assigned connection %s (shared)", conn.ID)
default:
log.Tracer(pkt.Ctx()).Tracef("filter: assigned connection %s", conn.ID)
sharedIndicator := ""
if shared {
sharedIndicator = " (shared)"
}
if created {
log.Tracer(pkt.Ctx()).Tracef("filter: created new connection %s%s", conn.ID, sharedIndicator)
} else {
log.Tracer(pkt.Ctx()).Tracef("filter: assigned connection %s%s", conn.ID, sharedIndicator)
}
return conn, nil
@ -307,7 +306,7 @@ func initialHandler(conn *network.Connection, pkt packet.Packet) {
log.Tracer(pkt.Ctx()).Trace("filter: handing over to connection-based handler")
// Check for pre-authenticated port.
if localPortIsPreAuthenticated(conn.Entity.Protocol, conn.LocalPort) {
if !conn.Inbound && localPortIsPreAuthenticated(conn.Entity.Protocol, conn.LocalPort) {
// Approve connection.
conn.Accept("connection by Portmaster", noReasonOptionKey)
conn.Internal = true

View file

@ -181,7 +181,7 @@ func (q *Queue) packetHandler(ctx context.Context) func(nfqueue.Attribute) int {
if attrs.Payload == nil {
// There is not payload.
log.Warningf("nfqueue: packet #%s has no payload", pkt.pktID)
log.Warningf("nfqueue: packet #%d has no payload", pkt.pktID)
return 0
}

View file

@ -37,7 +37,7 @@ import (
const noReasonOptionKey = ""
type deciderFn func(context.Context, *network.Connection, packet.Packet) bool
type deciderFn func(context.Context, *network.Connection, *profile.LayeredProfile, packet.Packet) bool
var defaultDeciders = []deciderFn{
checkPortmasterConnection,
@ -45,6 +45,7 @@ var defaultDeciders = []deciderFn{
checkConnectionType,
checkConnectionScope,
checkEndpointLists,
checkResolverScope,
checkConnectivityDomain,
checkBypassPrevention,
checkFilterLists,
@ -82,6 +83,13 @@ func DecideOnConnection(ctx context.Context, conn *network.Connection, pkt packe
if conn.Entity != nil {
conn.Entity.ResetLists()
}
} else {
// Check if the revision counter of the connection needs updating.
revCnt := layeredProfile.RevisionCnt()
if conn.ProfileRevisionCounter != revCnt {
conn.ProfileRevisionCounter = revCnt
conn.SaveWhenFinished()
}
}
// DNS request from the system resolver require a special decision process,
@ -90,7 +98,7 @@ func DecideOnConnection(ctx context.Context, conn *network.Connection, pkt packe
// 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)
done, _ := runDeciders(ctx, dnsFromSystemResolverDeciders, conn, layeredProfile, pkt)
if !done {
conn.Accept("allowing system resolver dns request", noReasonOptionKey)
}
@ -98,7 +106,7 @@ func DecideOnConnection(ctx context.Context, conn *network.Connection, pkt packe
}
// Run all deciders and return if they came to a conclusion.
done, defaultAction := runDeciders(ctx, defaultDeciders, conn, pkt)
done, defaultAction := runDeciders(ctx, defaultDeciders, conn, layeredProfile, pkt)
if done {
return
}
@ -114,16 +122,14 @@ func DecideOnConnection(ctx context.Context, conn *network.Connection, pkt packe
}
}
func runDeciders(ctx context.Context, selectedDeciders []deciderFn, conn *network.Connection, pkt packet.Packet) (done bool, defaultAction uint8) {
layeredProfile := conn.Process().Profile()
// Read-lock the all the profiles.
func runDeciders(ctx context.Context, selectedDeciders []deciderFn, conn *network.Connection, layeredProfile *profile.LayeredProfile, pkt packet.Packet) (done bool, defaultAction uint8) {
// Read-lock all the profiles.
layeredProfile.LockForUsage()
defer layeredProfile.UnlockForUsage()
// Go though all deciders, return if one sets an action.
for _, decider := range selectedDeciders {
if decider(ctx, conn, pkt) {
if decider(ctx, conn, layeredProfile, pkt) {
return true, profile.DefaultActionNotSet
}
}
@ -134,7 +140,7 @@ func runDeciders(ctx context.Context, selectedDeciders []deciderFn, conn *networ
// checkPortmasterConnection allows all connection that originate from
// portmaster itself.
func checkPortmasterConnection(ctx context.Context, conn *network.Connection, pkt packet.Packet) bool {
func checkPortmasterConnection(ctx context.Context, conn *network.Connection, _ *profile.LayeredProfile, pkt packet.Packet) bool {
// Grant own outgoing connections.
if conn.Process().Pid == ownPID &&
(pkt == nil || pkt.IsOutbound()) {
@ -148,7 +154,7 @@ func checkPortmasterConnection(ctx context.Context, conn *network.Connection, pk
}
// checkSelfCommunication checks if the process is communicating with itself.
func checkSelfCommunication(ctx context.Context, conn *network.Connection, pkt packet.Packet) bool {
func checkSelfCommunication(ctx context.Context, conn *network.Connection, _ *profile.LayeredProfile, pkt packet.Packet) bool {
// check if process is communicating with itself
if pkt != nil {
// TODO: evaluate the case where different IPs in the 127/8 net are used.
@ -183,13 +189,10 @@ func checkSelfCommunication(ctx context.Context, conn *network.Connection, pkt p
return false
}
func checkEndpointLists(ctx context.Context, conn *network.Connection, _ packet.Packet) bool {
func checkEndpointLists(ctx context.Context, conn *network.Connection, p *profile.LayeredProfile, _ packet.Packet) bool {
var result endpoints.EPResult
var reason endpoints.Reason
// there must always be a profile.
p := conn.Process().Profile()
// check endpoints list
var optionKey string
if conn.Inbound {
@ -211,35 +214,41 @@ func checkEndpointLists(ctx context.Context, conn *network.Connection, _ packet.
return false
}
func checkConnectionType(ctx context.Context, conn *network.Connection, _ packet.Packet) bool {
p := conn.Process().Profile()
func checkConnectionType(ctx context.Context, conn *network.Connection, p *profile.LayeredProfile, _ packet.Packet) bool {
switch {
case conn.Type != network.IPConnection:
// check conn type
switch conn.Scope {
case network.IncomingLAN, network.IncomingInternet, network.IncomingInvalid:
if p.BlockInbound() {
if conn.Scope == network.IncomingHost {
conn.Block("inbound connections blocked", profile.CfgOptionBlockInboundKey)
} else {
// Decider only applies to IP connections.
return false
case conn.Inbound &&
!conn.Entity.IPScope.IsLocalhost() &&
p.BlockInbound():
// BlockInbound does not apply to the Localhost scope.
conn.Drop("inbound connections blocked", profile.CfgOptionBlockInboundKey)
}
return true
}
case network.PeerInternet:
// BlockP2P only applies to connections to the Internet
if p.BlockP2P() {
case conn.Entity.IPScope.IsGlobal() &&
conn.Entity.Domain == "" &&
p.BlockP2P():
// BlockP2P only applies to the Global scope.
conn.Block("direct connections (P2P) blocked", profile.CfgOptionBlockP2PKey)
return true
}
}
default:
return false
}
}
func checkConnectivityDomain(_ context.Context, conn *network.Connection, _ packet.Packet) bool {
p := conn.Process().Profile()
func checkConnectivityDomain(_ context.Context, conn *network.Connection, p *profile.LayeredProfile, _ packet.Packet) bool {
switch {
case conn.Entity.Domain == "":
// Only applies if a domain is available.
return false
case netenv.GetOnlineStatus() > netenv.StatusPortal:
// Special grant only applies if network status is Portal (or even more limited).
return false
@ -263,9 +272,7 @@ func checkConnectivityDomain(_ context.Context, conn *network.Connection, _ pack
}
}
func checkConnectionScope(_ context.Context, conn *network.Connection, _ packet.Packet) bool {
p := conn.Process().Profile()
func checkConnectionScope(_ context.Context, conn *network.Connection, p *profile.LayeredProfile, _ packet.Packet) bool {
// If we are handling a DNS request, check if we can immediately block it.
if conn.Type == network.DNSRequest {
// DNS is expected to resolve to LAN or Internet addresses.
@ -300,8 +307,46 @@ func checkConnectionScope(_ context.Context, conn *network.Connection, _ packet.
return true
}
return false
}
func checkBypassPrevention(_ context.Context, conn *network.Connection, p *profile.LayeredProfile, _ packet.Packet) bool {
if p.PreventBypassing() {
// check for bypass protection
result, reason, reasonCtx := PreventBypassing(conn)
switch result {
case endpoints.Denied:
conn.BlockWithContext("bypass prevention: "+reason, profile.CfgOptionPreventBypassingKey, reasonCtx)
return true
case endpoints.Permitted:
conn.AcceptWithContext("bypass prevention: "+reason, profile.CfgOptionPreventBypassingKey, reasonCtx)
return true
case endpoints.NoMatch:
}
}
return false
}
func checkFilterLists(ctx context.Context, conn *network.Connection, p *profile.LayeredProfile, pkt packet.Packet) bool {
// apply privacy filter lists
result, reason := p.MatchFilterLists(ctx, conn.Entity)
switch result {
case endpoints.Denied:
conn.DenyWithContext(reason.String(), profile.CfgOptionFilterListsKey, reason.Context())
return true
case endpoints.NoMatch:
// nothing to do
default:
log.Tracer(ctx).Debugf("filter: filter lists returned unsupported verdict: %s", result)
}
return false
}
func checkResolverScope(_ context.Context, conn *network.Connection, p *profile.LayeredProfile, _ packet.Packet) bool {
// If the IP address was resolved, check the scope of the resolver.
switch {
case conn.Type != network.IPConnection:
// Only applies to IP connections.
case !p.RemoveOutOfScopeDNS():
// Out of scope checking is not active.
case conn.Resolver == nil:
@ -321,43 +366,7 @@ func checkConnectionScope(_ context.Context, conn *network.Connection, _ packet.
return false
}
func checkBypassPrevention(_ context.Context, conn *network.Connection, _ packet.Packet) bool {
if conn.Process().Profile().PreventBypassing() {
// check for bypass protection
result, reason, reasonCtx := PreventBypassing(conn)
switch result {
case endpoints.Denied:
conn.BlockWithContext("bypass prevention: "+reason, profile.CfgOptionPreventBypassingKey, reasonCtx)
return true
case endpoints.Permitted:
conn.AcceptWithContext("bypass prevention: "+reason, profile.CfgOptionPreventBypassingKey, reasonCtx)
return true
case endpoints.NoMatch:
}
}
return false
}
func checkFilterLists(ctx context.Context, conn *network.Connection, pkt packet.Packet) bool {
// apply privacy filter lists
p := conn.Process().Profile()
result, reason := p.MatchFilterLists(ctx, conn.Entity)
switch result {
case endpoints.Denied:
conn.DenyWithContext(reason.String(), profile.CfgOptionFilterListsKey, reason.Context())
return true
case endpoints.NoMatch:
// nothing to do
default:
log.Tracer(ctx).Debugf("filter: filter lists returned unsupported verdict: %s", result)
}
return false
}
func checkDomainHeuristics(ctx context.Context, conn *network.Connection, _ packet.Packet) bool {
p := conn.Process().Profile()
func checkDomainHeuristics(ctx context.Context, conn *network.Connection, p *profile.LayeredProfile, _ packet.Packet) bool {
if !p.DomainHeuristics() {
return false
}
@ -415,7 +424,7 @@ func checkDomainHeuristics(ctx context.Context, conn *network.Connection, _ pack
return false
}
func dropInbound(_ context.Context, conn *network.Connection, _ packet.Packet) bool {
func dropInbound(_ context.Context, conn *network.Connection, _ *profile.LayeredProfile, _ packet.Packet) bool {
// implicit default=block for inbound
if conn.Inbound {
conn.Drop("incoming connection blocked by default", profile.CfgOptionServiceEndpointsKey)
@ -424,9 +433,7 @@ func dropInbound(_ context.Context, conn *network.Connection, _ packet.Packet) b
return false
}
func checkAutoPermitRelated(_ context.Context, conn *network.Connection, _ packet.Packet) bool {
p := conn.Process().Profile()
func checkAutoPermitRelated(_ context.Context, conn *network.Connection, p *profile.LayeredProfile, _ packet.Packet) bool {
// Auto permit is disabled for default action permit.
if p.DefaultAction() == profile.DefaultActionPermit {
return false

View file

@ -106,10 +106,10 @@ func createPrompt(ctx context.Context, conn *network.Connection, pkt packet.Pack
switch {
case conn.Inbound, conn.Entity.Domain == "": // connection to/from IP
nID = fmt.Sprintf(
"%s-%s-%s-%s",
"%s-%s-%v-%s",
promptIDPrefix,
localProfile.ID,
conn.Scope,
conn.Inbound,
pkt.Info().RemoteIP(),
)
default: // connection to domain
@ -117,7 +117,7 @@ func createPrompt(ctx context.Context, conn *network.Connection, pkt packet.Pack
"%s-%s-%s",
promptIDPrefix,
localProfile.ID,
conn.Scope,
conn.Entity.Domain,
)
}

View file

@ -189,8 +189,8 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
// Resolve request.
rrCache, err = resolver.Resolve(ctx, q)
// Handle error.
if err != nil {
// React to special errors.
switch {
case errors.Is(err, resolver.ErrNotFound):
tracer.Tracef("nameserver: %s", err)
@ -214,21 +214,25 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
return reply(nsutil.ServerFailure("internal error: " + err.Error()))
}
}
if rrCache == nil {
// Handle special cases.
switch {
case rrCache == nil:
tracer.Warning("nameserver: received successful, but empty reply from resolver")
return reply(nsutil.ServerFailure("internal error: empty reply"))
case rrCache.RCode == dns.RcodeNameError:
return reply(nsutil.NxDomain("no answer found (NXDomain)"))
}
tracer.Trace("nameserver: deciding on resolved dns")
rrCache = firewall.FilterResolvedDNS(ctx, conn, q, rrCache)
if rrCache == nil {
// Check again if there is a responder from the firewall.
if responder, ok := conn.Reason.Context.(nsutil.Responder); ok {
tracer.Infof("nameserver: handing over request for %s to 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)
}
// Request was blocked by the firewall.
// Check if there is a Verdict to act upon.
switch conn.Verdict {
case network.VerdictBlock, network.VerdictDrop, network.VerdictFailed:
tracer.Infof(
@ -237,8 +241,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
q.ID(),
conn.Process(),
)
return reply(conn, conn)
}
return reply(conn, conn, rrCache)
}
// Revert back to non-standard question format, if we had to convert.
@ -247,10 +250,15 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
}
// Reply with successful response.
noAnswerIndicator := ""
if len(rrCache.Answer) == 0 {
noAnswerIndicator = "/no answer"
}
tracer.Infof(
"nameserver: returning %s response (%s) for %s to %s",
"nameserver: returning %s response (%s%s) for %s to %s",
conn.Verdict.Verb(),
dns.RcodeToString[rrCache.RCode],
noAnswerIndicator,
q.ID(),
conn.Process(),
)

View file

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"strings"
"time"
"github.com/miekg/dns"
"github.com/safing/portbase/log"
@ -73,10 +74,8 @@ func ZeroIP(msgs ...string) ResponderFunc {
}
switch {
case hasErr && len(reply.Answer) == 0:
case hasErr || len(reply.Answer) == 0:
reply.SetRcode(request, dns.RcodeServerFailure)
case len(reply.Answer) == 0:
reply.SetRcode(request, dns.RcodeNameError)
default:
reply.SetRcode(request, dns.RcodeSuccess)
}
@ -115,10 +114,8 @@ func Localhost(msgs ...string) ResponderFunc {
}
switch {
case hasErr && len(reply.Answer) == 0:
case hasErr || len(reply.Answer) == 0:
reply.SetRcode(request, dns.RcodeServerFailure)
case len(reply.Answer) == 0:
reply.SetRcode(request, dns.RcodeNameError)
default:
reply.SetRcode(request, dns.RcodeSuccess)
}
@ -134,6 +131,15 @@ func NxDomain(msgs ...string) ResponderFunc {
return func(ctx context.Context, request *dns.Msg) *dns.Msg {
reply := new(dns.Msg).SetRcode(request, dns.RcodeNameError)
AddMessagesToReply(ctx, reply, log.InfoLevel, msgs...)
// According to RFC4074 (https://tools.ietf.org/html/rfc4074), there are
// nameservers that incorrectly respond with NXDomain instead of an empty
// SUCCESS response when there are other RRs for the queried domain name.
// This can lead to the software thinking that no RRs exist for that
// domain. In order to mitigate this a bit, we slightly delay NXDomain
// responses.
time.Sleep(500 * time.Millisecond)
return reply
}
}

View file

@ -53,6 +53,7 @@ func GetAssignedGlobalAddresses() (ipv4 []net.IP, ipv6 []net.IP, err error) {
var (
myNetworks []*net.IPNet
myNetworksLock sync.Mutex
myNetworksNetworkChangedFlag = GetNetworkChangedFlag()
)
// IsMyIP returns whether the given unicast IP is currently configured on the local host.
@ -69,8 +70,12 @@ func IsMyIP(ip net.IP) (yes bool, err error) {
myNetworksLock.Lock()
defer myNetworksLock.Unlock()
// Check for match.
if mine, matched := checkIfMyIP(ip); matched {
// Check if the network changed.
if myNetworksNetworkChangedFlag.IsSet() {
// Reset changed flag.
myNetworksNetworkChangedFlag.Refresh()
} else if mine, matched := checkIfMyIP(ip); matched {
// If the network did not change, check for match immediately.
return mine, nil
}

View file

@ -13,7 +13,7 @@ func init() {
flag.BoolVar(&privileged, "privileged", false, "run tests that require root/admin privileges")
}
func TestGetApproximateInternetLocation(t *testing.T) {
func TestGetInternetLocation(t *testing.T) {
if testing.Short() {
t.Skip()
}
@ -21,9 +21,9 @@ func TestGetApproximateInternetLocation(t *testing.T) {
t.Skip("skipping privileged test, active with -privileged argument")
}
loc, err := GetInternetLocation()
if err != nil {
t.Fatalf("GetApproximateInternetLocation failed: %s", err)
loc, ok := GetInternetLocation()
if !ok {
t.Fatal("GetApproximateInternetLocation failed")
}
t.Logf("GetApproximateInternetLocation: %+v", loc)
}

View file

@ -265,6 +265,10 @@ func NewConnectionFromDNSRequest(ctx context.Context, fqdn string, cnames []stri
dnsConn.Internal = true
}
// DNS Requests are saved by the nameserver depending on the result of the
// query. Blocked requests are saved immediately, accepted ones are only
// saved if they are not "used" by a connection.
return dnsConn
}
@ -295,6 +299,10 @@ func NewConnectionFromExternalDNSRequest(ctx context.Context, fqdn string, cname
dnsConn.Internal = localProfile.Internal
}
// DNS Requests are saved by the nameserver depending on the result of the
// query. Blocked requests are saved immediately, accepted ones are only
// saved if they are not "used" by a connection.
return dnsConn, nil
}
@ -307,20 +315,19 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection {
proc = process.GetUnidentifiedProcess(pkt.Ctx())
}
// Create the (remote) entity.
entity := &intel.Entity{
Protocol: uint8(pkt.Info().Protocol),
Port: pkt.Info().RemotePort(),
}
entity.SetIP(pkt.Info().RemoteIP())
entity.SetDstPort(pkt.Info().DstPort)
var scope string
var entity *intel.Entity
var resolverInfo *resolver.ResolverInfo
if inbound {
// inbound connection
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:
scope = IncomingHost
@ -337,19 +344,11 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection {
} else {
// outbound connection
entity = &intel.Entity{
Protocol: uint8(pkt.Info().Protocol),
Port: pkt.Info().DstPort,
}
entity.SetIP(pkt.Info().Dst)
entity.SetDstPort(entity.Port)
// check if we can find a domain for that IP
ipinfo, err := resolver.GetIPInfo(proc.Profile().LocalProfile().ID, pkt.Info().Dst.String())
ipinfo, err := resolver.GetIPInfo(proc.Profile().LocalProfile().ID, pkt.Info().RemoteIP().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())
ipinfo, err = resolver.GetIPInfo(resolver.IPInfoProfileScopeGlobal, pkt.Info().RemoteIP().String())
}
if err == nil {
lastResolvedDomain := ipinfo.MostRecentDomain()
@ -364,7 +363,7 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection {
// check if destination IP is the captive portal's IP
portal := netenv.GetCaptivePortal()
if pkt.Info().Dst.Equal(portal.IP) {
if pkt.Info().RemoteIP().Equal(portal.IP) {
scope = portal.Domain
entity.Domain = portal.Domain
}
@ -416,6 +415,10 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection {
newConn.Internal = localProfile.Internal
}
// Save connection to internal state in order to mitigate creation of
// duplicates. Do not propagate yet, as there is no verdict yet.
conns.add(newConn)
return newConn
}
@ -647,12 +650,12 @@ func (conn *Connection) SetInspectorData(new map[uint8]interface{}) {
// String returns a string representation of conn.
func (conn *Connection) String() string {
switch conn.Scope {
case IncomingHost, IncomingLAN, IncomingInternet, IncomingInvalid:
switch {
case conn.Inbound:
return fmt.Sprintf("%s <- %s", conn.process, conn.Entity.IP)
case PeerHost, PeerLAN, PeerInternet, PeerInvalid:
return fmt.Sprintf("%s -> %s", conn.process, conn.Entity.IP)
default:
case conn.Entity.Domain != "":
return fmt.Sprintf("%s to %s (%s)", conn.process, conn.Entity.Domain, conn.Entity.IP)
default:
return fmt.Sprintf("%s -> %s", conn.process, conn.Entity.IP)
}
}

View file

@ -55,11 +55,14 @@ func SaveOpenDNSRequest(conn *Connection) {
openDNSRequestsLock.Lock()
defer openDNSRequestsLock.Unlock()
key := getDNSRequestCacheKey(conn.process.Pid, conn.Scope)
key := getDNSRequestCacheKey(conn.process.Pid, conn.Entity.Domain)
if existingConn, ok := openDNSRequests[key]; ok {
// End previous request and save it.
existingConn.Lock()
defer existingConn.Unlock()
existingConn.Ended = conn.Started
existingConn.Unlock()
existingConn.Save()
return
}

View file

@ -41,6 +41,11 @@ nextPort:
}
}
// Log if it took more than 10 attempts.
if i >= 10 {
log.Warningf("network: took %d attempts to find a suitable unused port for pre-auth", i+1)
}
// The checks have passed. We have found a good unused port.
return port, true
}

View file

@ -379,12 +379,11 @@ func TestEndpointMatching(t *testing.T) {
t.Fatal(err)
}
testEndpointMatch(t, ep, (&intel.Entity{
IP: net.ParseIP("192.168.0.1"),
}).Init(), Permitted)
testEndpointMatch(t, ep, (&intel.Entity{
IP: net.ParseIP("151.101.1.164"), // nytimes.com
}).Init(), NoMatch)
entity = &intel.Entity{}
entity.SetIP(net.ParseIP("192.168.0.1"))
testEndpointMatch(t, ep, entity, Permitted)
entity.SetIP(net.ParseIP("151.101.1.164")) // nytimes.com
testEndpointMatch(t, ep, entity, NoMatch)
// Lists

View file

@ -1,7 +1,10 @@
package resolver
import (
"net/http"
"github.com/safing/portbase/api"
"github.com/safing/portbase/database/record"
)
func registerAPI() error {
@ -25,6 +28,24 @@ func registerAPI() error {
return err
}
if err := api.RegisterEndpoint(api.Endpoint{
Path: `dns/cache`,
Read: api.PermitUser,
RecordFunc: func(r *api.Request) (record.Record, error) {
return recordDatabase.Get(nameRecordsKeyPrefix + r.URL.Query().Get("q"))
},
Name: "Get DNS Record from Cache",
Description: "Returns cached dns records from the internal cache.",
Parameters: []api.Parameter{{
Method: http.MethodGet,
Field: "q",
Value: "fqdn and query type",
Description: "Specify the query like this: `example.com.A`.",
}},
}); err != nil {
return err
}
return nil
}

View file

@ -9,6 +9,9 @@ func TestNameRecordStorage(t *testing.T) {
testNameRecord := &NameRecord{
Domain: testDomain,
Question: testQuestion,
Resolver: &ResolverInfo{
Type: "dns",
},
}
err := testNameRecord.Save()

View file

@ -341,6 +341,7 @@ resolveLoop:
}
// resolve
log.Tracer(ctx).Tracef("resolver: sending query for %s to %s", q.ID(), resolver.Info.ID())
rrCache, err = resolver.Conn.Query(ctx, q)
if err != nil {
switch {
@ -416,9 +417,11 @@ resolveLoop:
return nil, err
}
// Save the new entry if cache is enabled.
if !q.NoCaching && rrCache.Cacheable() {
// Adjust TTLs.
rrCache.Clean(minTTL)
// Save the new entry if cache is enabled and the record may be cached.
if !q.NoCaching && rrCache.Cacheable() {
err = rrCache.Save()
if err != nil {
log.Tracer(ctx).Warningf("resolver: failed to cache RR for %s: %s", q.ID(), err)

View file

@ -33,5 +33,5 @@ func TestResolveIPAndValidate(t *testing.T) {
testReverse(t, "2606:4700:4700::1111", "one.one.one.one.", "")
testReverse(t, "93.184.216.34", "example.com.", "record could not be found: 34.216.184.93.in-addr.arpa.PTR")
testReverse(t, "185.199.109.153", "sites.github.io.", "record could not be found: 153.109.199.185.in-addr.arpa.PTR")
testReverse(t, "185.199.109.153", "cdn-185-199-109-153.github.com.", "record could not be found: 153.109.199.185.in-addr.arpa.PTR")
}

View file

@ -82,12 +82,6 @@ func (rrCache *RRCache) Clean(minExpires uint32) {
lowestTTL = maxTTL
}
// Adjust return code if there are no answers
if rrCache.RCode == dns.RcodeSuccess &&
len(rrCache.Answer) == 0 {
rrCache.RCode = dns.RcodeNameError
}
// shorten caching
switch {
case rrCache.RCode != dns.RcodeSuccess:
@ -96,6 +90,9 @@ func (rrCache *RRCache) Clean(minExpires uint32) {
case netenv.IsConnectivityDomain(rrCache.Domain):
// Responses from these domains might change very quickly depending on the environment.
lowestTTL = 3
case len(rrCache.Answer) == 0:
// Empty answer section: Domain exists, but not the queried RR.
lowestTTL = 60
case !netenv.Online():
// Not being fully online could mean that we get funny responses.
lowestTTL = 60
@ -318,9 +315,12 @@ func (rrCache *RRCache) GetExtraRRs(ctx context.Context, query *dns.Msg) (extra
// Add information about filtered entries.
if rrCache.Filtered {
if len(rrCache.FilteredEntries) > 1 {
extra = addExtra(ctx, extra, fmt.Sprintf("%d records have been filtered", len(rrCache.FilteredEntries)))
extra = addExtra(ctx, extra, fmt.Sprintf("%d RRs have been filtered:", len(rrCache.FilteredEntries)))
} else {
extra = addExtra(ctx, extra, fmt.Sprintf("%d record has been filtered", len(rrCache.FilteredEntries)))
extra = addExtra(ctx, extra, fmt.Sprintf("%d RR has been filtered:", len(rrCache.FilteredEntries)))
}
for _, filteredRecord := range rrCache.FilteredEntries {
extra = addExtra(ctx, extra, filteredRecord)
}
}

View file

@ -13,6 +13,9 @@ func TestCaching(t *testing.T) {
testNameRecord := &NameRecord{
Domain: testDomain,
Question: testQuestion,
Resolver: &ResolverInfo{
Type: "dns",
},
}
err := testNameRecord.Save()

5
test
View file

@ -176,13 +176,16 @@ echo "running tests for ${platformInfo//$'\n'/ }:"
# run vet/test on packages
for package in $packages; do
package=${package#github.com/safing/portmaster}
package=${package#/}
package=$PWD/$package
echo ""
echo $package
if [[ $testonly -eq 0 ]]; then
checkformat $package
run golint -set_exit_status -min_confidence 1.0 $package
run go vet $package
run golangci-lint run $GOPATH/src/$package
run golangci-lint run $package
fi
run go test -cover $fullTestFlags $package
done