mirror of
https://github.com/safing/portmaster
synced 2025-09-04 19:49:15 +00:00
Improve debug information in DNS responses
This commit is contained in:
parent
142bc1e54a
commit
3f3d82bdf1
8 changed files with 382 additions and 216 deletions
|
@ -13,7 +13,9 @@ import (
|
||||||
func PreventBypassing(conn *network.Connection) (endpoints.EPResult, string, nsutil.Responder) {
|
func PreventBypassing(conn *network.Connection) (endpoints.EPResult, string, nsutil.Responder) {
|
||||||
// Block firefox canary domain to disable DoH
|
// Block firefox canary domain to disable DoH
|
||||||
if strings.ToLower(conn.Entity.Domain) == "use-application-dns.net." {
|
if strings.ToLower(conn.Entity.Domain) == "use-application-dns.net." {
|
||||||
return endpoints.Denied, "blocked canary domain to prevent enabling DNS-over-HTTPs", nsutil.NxDomain()
|
return endpoints.Denied,
|
||||||
|
"blocked canary domain to prevent enabling of DNS-over-HTTPs",
|
||||||
|
nsutil.NxDomain("blocked canary domain to prevent enabling of DNS-over-HTTPs")
|
||||||
}
|
}
|
||||||
|
|
||||||
return endpoints.NoMatch, "", nil
|
return endpoints.NoMatch, "", nil
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package intel
|
package intel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -66,31 +67,31 @@ func (br ListBlockReason) MarshalJSON() ([]byte, error) {
|
||||||
// GetExtraRR implements the nsutil.RRProvider interface
|
// GetExtraRR implements the nsutil.RRProvider interface
|
||||||
// and adds additional TXT records justifying the reason
|
// and adds additional TXT records justifying the reason
|
||||||
// the request was blocked.
|
// the request was blocked.
|
||||||
func (br ListBlockReason) GetExtraRR(_ *dns.Msg, _ string, _ interface{}) []dns.RR {
|
func (br ListBlockReason) GetExtraRRs(ctx context.Context, _ *dns.Msg) []dns.RR {
|
||||||
rrs := make([]dns.RR, 0, len(br))
|
rrs := make([]dns.RR, 0, len(br))
|
||||||
|
|
||||||
for _, lm := range br {
|
for _, lm := range br {
|
||||||
blockedBy, err := dns.NewRR(fmt.Sprintf(
|
blockedBy, err := nsutil.MakeMessageRecord(log.InfoLevel, fmt.Sprintf(
|
||||||
`%s 0 IN TXT "blocked by filter lists %s"`,
|
"%s is blocked by filter lists %s",
|
||||||
lm.Entity,
|
lm.Entity,
|
||||||
strings.Join(lm.ActiveLists, ", "),
|
strings.Join(lm.ActiveLists, ", "),
|
||||||
))
|
))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
rrs = append(rrs, blockedBy)
|
rrs = append(rrs, blockedBy)
|
||||||
} else {
|
} else {
|
||||||
log.Errorf("intel: failed to create TXT RR for block reason: %s", err)
|
log.Tracer(ctx).Errorf("intel: failed to create TXT RR for block reason: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(lm.InactiveLists) > 0 {
|
if len(lm.InactiveLists) > 0 {
|
||||||
wouldBeBlockedBy, err := dns.NewRR(fmt.Sprintf(
|
wouldBeBlockedBy, err := nsutil.MakeMessageRecord(log.InfoLevel, fmt.Sprintf(
|
||||||
`%s 0 IN TXT "would be blocked by filter lists %s"`,
|
"%s would be blocked by filter lists %s",
|
||||||
lm.Entity,
|
lm.Entity,
|
||||||
strings.Join(lm.InactiveLists, ", "),
|
strings.Join(lm.InactiveLists, ", "),
|
||||||
))
|
))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
rrs = append(rrs, wouldBeBlockedBy)
|
rrs = append(rrs, wouldBeBlockedBy)
|
||||||
} else {
|
} else {
|
||||||
log.Errorf("intel: failed to create TXT RR for block reason: %s", err)
|
log.Tracer(ctx).Errorf("intel: failed to create TXT RR for block reason: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package nameserver
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -28,11 +27,10 @@ var (
|
||||||
dnsServer *dns.Server
|
dnsServer *dns.Server
|
||||||
|
|
||||||
listenAddress = "0.0.0.0:53"
|
listenAddress = "0.0.0.0:53"
|
||||||
localhostRRs []dns.RR
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
module = modules.Register("nameserver", prep, start, stop, "core", "resolver")
|
module = modules.Register("nameserver", nil, start, stop, "core", "resolver")
|
||||||
subsystems.Register(
|
subsystems.Register(
|
||||||
"dns",
|
"dns",
|
||||||
"Secure DNS",
|
"Secure DNS",
|
||||||
|
@ -43,22 +41,6 @@ func init() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func prep() error {
|
|
||||||
localhostIPv4, err := dns.NewRR("localhost. 17 IN A 127.0.0.1")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
localhostIPv6, err := dns.NewRR("localhost. 17 IN AAAA ::1")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
localhostRRs = []dns.RR{localhostIPv4, localhostIPv6}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func start() error {
|
func start() error {
|
||||||
dnsServer = &dns.Server{Addr: listenAddress, Net: "udp"}
|
dnsServer = &dns.Server{Addr: listenAddress, Net: "udp"}
|
||||||
dns.HandleFunc(".", handleRequestAsWorker)
|
dns.HandleFunc(".", handleRequestAsWorker)
|
||||||
|
@ -89,12 +71,6 @@ func stop() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func returnServerFailure(w dns.ResponseWriter, query *dns.Msg) {
|
|
||||||
m := new(dns.Msg)
|
|
||||||
m.SetRcode(query, dns.RcodeServerFailure)
|
|
||||||
_ = writeDNSResponse(w, m)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleRequestAsWorker(w dns.ResponseWriter, query *dns.Msg) {
|
func handleRequestAsWorker(w dns.ResponseWriter, query *dns.Msg) {
|
||||||
err := module.RunWorker("dns request", func(ctx context.Context) error {
|
err := module.RunWorker("dns request", func(ctx context.Context) error {
|
||||||
return handleRequest(ctx, w, query)
|
return handleRequest(ctx, w, query)
|
||||||
|
@ -104,86 +80,80 @@ func handleRequestAsWorker(w dns.ResponseWriter, query *dns.Msg) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) error { //nolint:gocognit // TODO
|
func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg) error { //nolint:gocognit // TODO
|
||||||
// only process first question, that's how everyone does it.
|
// Only process first question, that's how everyone does it.
|
||||||
question := query.Question[0]
|
question := request.Question[0]
|
||||||
q := &resolver.Query{
|
q := &resolver.Query{
|
||||||
FQDN: question.Name,
|
FQDN: question.Name,
|
||||||
QType: dns.Type(question.Qtype),
|
QType: dns.Type(question.Qtype),
|
||||||
}
|
}
|
||||||
|
|
||||||
// return with server failure if offline
|
// Get remote address of request.
|
||||||
if netenv.GetOnlineStatus() == netenv.StatusOffline &&
|
|
||||||
!netenv.IsConnectivityDomain(q.FQDN) {
|
|
||||||
log.Tracer(ctx).Debugf("resolver: not resolving %s, device is offline", q.FQDN)
|
|
||||||
returnServerFailure(w, query)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// check class
|
|
||||||
if question.Qclass != dns.ClassINET {
|
|
||||||
// we only serve IN records, return nxdomain
|
|
||||||
log.Warningf("nameserver: only IN record requests are supported but received Qclass %d, returning NXDOMAIN", question.Qclass)
|
|
||||||
sendResponse(w, query, 0, "qclass not served", nsutil.Refused())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle request for localhost
|
|
||||||
if strings.HasSuffix(q.FQDN, "localhost.") {
|
|
||||||
m := new(dns.Msg)
|
|
||||||
m.SetReply(query)
|
|
||||||
m.Answer = localhostRRs
|
|
||||||
if err := writeDNSResponse(w, m); err != nil {
|
|
||||||
log.Warningf("nameserver: failed to handle request to %s: %s", q.FQDN, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// get remote address
|
|
||||||
remoteAddr, ok := w.RemoteAddr().(*net.UDPAddr)
|
remoteAddr, ok := w.RemoteAddr().(*net.UDPAddr)
|
||||||
if !ok {
|
if !ok {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the request is local
|
// Start context tracer for context-aware logging.
|
||||||
local, err := netenv.IsMyIP(remoteAddr.IP)
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("nameserver: failed to check if request for %s%s is local: %s", q.FQDN, q.QType, err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if !local {
|
|
||||||
log.Warningf("nameserver: external request for %s%s, ignoring", q.FQDN, q.QType)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if valid domain name
|
|
||||||
if !netutils.IsValidFqdn(q.FQDN) {
|
|
||||||
log.Debugf("nameserver: domain name %s is invalid, returning nxdomain", q.FQDN)
|
|
||||||
sendResponse(w, query, 0, "invalid FQDN", nsutil.Refused())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// start tracer
|
|
||||||
ctx, tracer := log.AddTracer(ctx)
|
ctx, tracer := log.AddTracer(ctx)
|
||||||
defer tracer.Submit()
|
defer tracer.Submit()
|
||||||
tracer.Tracef("nameserver: handling new request for %s%s from %s:%d, getting connection", q.FQDN, q.QType, remoteAddr.IP, remoteAddr.Port)
|
tracer.Tracef("nameserver: handling new request for %s%s from %s:%d", q.FQDN, q.QType, remoteAddr.IP, remoteAddr.Port)
|
||||||
|
|
||||||
// TODO: if there are 3 request for the same domain/type in a row, delete all caches of that domain
|
// Setup quick reply function.
|
||||||
|
reply := func(responder nsutil.Responder, rrProviders ...nsutil.RRProvider) error {
|
||||||
|
return sendResponse(ctx, w, request, responder, rrProviders...)
|
||||||
|
}
|
||||||
|
|
||||||
// get connection
|
// Return with server failure if offline.
|
||||||
|
if netenv.GetOnlineStatus() == netenv.StatusOffline &&
|
||||||
|
!netenv.IsConnectivityDomain(q.FQDN) {
|
||||||
|
tracer.Debugf("resolver: not resolving %s, device is offline", q.FQDN)
|
||||||
|
return reply(nsutil.ServerFailure("resolving disabled, device is offline"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the Query Class.
|
||||||
|
if question.Qclass != dns.ClassINET {
|
||||||
|
// we only serve IN records, return nxdomain
|
||||||
|
tracer.Warningf("nameserver: only IN record requests are supported but received Qclass %d, returning NXDOMAIN", question.Qclass)
|
||||||
|
return reply(nsutil.Refused("unsupported qclass"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle request for localhost.
|
||||||
|
if strings.HasSuffix(q.FQDN, "localhost.") {
|
||||||
|
return reply(nsutil.Localhost(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate request - only requests from the local host, but with any of its IPs, are allowed.
|
||||||
|
local, err := netenv.IsMyIP(remoteAddr.IP)
|
||||||
|
if err != nil {
|
||||||
|
tracer.Warningf("nameserver: failed to check if request for %s%s is local: %s", q.FQDN, q.QType, err)
|
||||||
|
return nil // Do no reply, drop request immediately.
|
||||||
|
}
|
||||||
|
if !local {
|
||||||
|
tracer.Warningf("nameserver: external request for %s%s, ignoring", q.FQDN, q.QType)
|
||||||
|
return nil // Do no reply, drop request immediately.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate domain name.
|
||||||
|
if !netutils.IsValidFqdn(q.FQDN) {
|
||||||
|
tracer.Debugf("nameserver: domain name %s is invalid, refusing", q.FQDN)
|
||||||
|
return reply(nsutil.Refused("invalid domain"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get connection for this request. This identifies the process behind the request.
|
||||||
conn := network.NewConnectionFromDNSRequest(ctx, q.FQDN, nil, packet.IPv4, remoteAddr.IP, uint16(remoteAddr.Port))
|
conn := network.NewConnectionFromDNSRequest(ctx, q.FQDN, nil, packet.IPv4, remoteAddr.IP, uint16(remoteAddr.Port))
|
||||||
|
|
||||||
// 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 right 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
|
||||||
// the 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:
|
||||||
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:
|
network.VerdictRerouteToNameserver, network.VerdictRerouteToTunnel:
|
||||||
|
@ -194,104 +164,72 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// TODO: this has been obsoleted due to special profiles
|
// Check request with the privacy filter before resolving.
|
||||||
if conn.Process().Profile() == nil {
|
|
||||||
tracer.Infof("nameserver: failed to find process for request %s, returning NXDOMAIN", conn)
|
|
||||||
// NOTE(ppacher): saving unknown process connection might end up in a lot of
|
|
||||||
// processes. Consider disabling that via config.
|
|
||||||
conn.Failed("Unknown process")
|
|
||||||
sendResponse(w, query, conn.Verdict, conn.Reason, conn.ReasonContext)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// save security level to query
|
|
||||||
q.SecurityLevel = conn.Process().Profile().SecurityLevel()
|
|
||||||
|
|
||||||
// check profile before we even get intel and rr
|
|
||||||
firewall.DecideOnConnection(ctx, conn, nil)
|
firewall.DecideOnConnection(ctx, conn, nil)
|
||||||
|
|
||||||
|
// Check if there is Verdict to act upon.
|
||||||
switch conn.Verdict {
|
switch conn.Verdict {
|
||||||
case network.VerdictBlock:
|
case network.VerdictBlock, network.VerdictDrop, network.VerdictFailed:
|
||||||
tracer.Infof("nameserver: %s blocked, returning nxdomain", conn)
|
tracer.Infof("nameserver: request for %s from %s %s", q.ID(), conn.Process(), conn.Verdict.Verb())
|
||||||
sendResponse(w, query, conn.Verdict, conn.Reason, conn.ReasonContext)
|
return reply(conn, conn)
|
||||||
return nil
|
|
||||||
case network.VerdictDrop, network.VerdictFailed:
|
|
||||||
tracer.Infof("nameserver: %s dropped, not replying", conn)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// the firewall now decided on the connection and set it to accept
|
// Check if there is a responder from the firewall.
|
||||||
// If we have a reason context and that context implements nsutil.Responder
|
// In special cases, the firewall might want to respond the query itself.
|
||||||
// we may need to responde with something else.
|
|
||||||
// A reason for this might be that the request is sink-holed to a forced
|
// A reason for this might be that the request is sink-holed to a forced
|
||||||
// ip address in which case we "Accept" it but handle the resolving
|
// IP address in which case we "accept" it, but let the firewall handle
|
||||||
// differently.
|
// the resolving as it wishes.
|
||||||
if responder, ok := conn.ReasonContext.(nsutil.Responder); ok {
|
if responder, ok := conn.ReasonContext.(nsutil.Responder); ok {
|
||||||
tracer.Infof("nameserver: %s handing over to reason-responder: %s", q.FQDN, conn.Reason)
|
// Save the request as open, as we don't know if there will be a connection or not.
|
||||||
reply := responder.ReplyWithDNS(query, conn.Reason, conn.ReasonContext)
|
|
||||||
if err := w.WriteMsg(reply); err != nil {
|
|
||||||
tracer.Warningf("nameserver: failed to return response %s%s to %s: %s", q.FQDN, q.QType, conn.Process(), err)
|
|
||||||
} else {
|
|
||||||
tracer.Debugf("nameserver: returning response %s%s to %s", q.FQDN, q.QType, conn.Process())
|
|
||||||
}
|
|
||||||
|
|
||||||
// save dns request as open
|
|
||||||
network.SaveOpenDNSRequest(conn)
|
network.SaveOpenDNSRequest(conn)
|
||||||
|
|
||||||
return nil
|
tracer.Infof("nameserver: handing over request for %s to filter responder: %s", q.ID(), conn.Reason)
|
||||||
|
return reply(responder)
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolve
|
// Save security level to query, so that the resolver can react to configuration.
|
||||||
|
q.SecurityLevel = conn.Process().Profile().SecurityLevel()
|
||||||
|
|
||||||
|
// Resolve request.
|
||||||
rrCache, err := resolver.Resolve(ctx, q)
|
rrCache, err := resolver.Resolve(ctx, q)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: analyze nxdomain requests, malware could be trying DGA-domains
|
// React to special errors.
|
||||||
tracer.Debugf("nameserver: %s requested %s%s: %s", conn.Process(), q.FQDN, q.QType, err)
|
switch {
|
||||||
|
case errors.Is(err, resolver.ErrNotFound):
|
||||||
if errors.Is(err, resolver.ErrBlocked) {
|
return reply(nsutil.NxDomain(""), nil)
|
||||||
conn.Block(err.Error())
|
case errors.Is(err, resolver.ErrBlocked):
|
||||||
} else {
|
return reply(nsutil.ZeroIP(""), nil)
|
||||||
conn.Failed("failed to resolve: " + err.Error())
|
case errors.Is(err, resolver.ErrLocalhost):
|
||||||
|
return reply(nsutil.Localhost(""), nil)
|
||||||
|
default:
|
||||||
|
return reply(nsutil.ServerFailure("internal error: "+err.Error()), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
sendResponse(w, query, conn.Verdict, conn.Reason, conn.ReasonContext)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tracer.Trace("nameserver: deciding on resolved dns")
|
tracer.Trace("nameserver: deciding on resolved dns")
|
||||||
rrCache = firewall.DecideOnResolvedDNS(ctx, conn, q, rrCache)
|
rrCache = firewall.DecideOnResolvedDNS(ctx, conn, q, rrCache)
|
||||||
if rrCache == nil {
|
if rrCache == nil {
|
||||||
sendResponse(w, query, conn.Verdict, conn.Reason, conn.ReasonContext)
|
// Check again if there is a responder from the firewall.
|
||||||
return nil
|
if responder, ok := conn.ReasonContext.(nsutil.Responder); ok {
|
||||||
}
|
// Save the request as open, as we don't know if there will be a connection or not.
|
||||||
|
network.SaveOpenDNSRequest(conn)
|
||||||
|
|
||||||
// reply to query
|
tracer.Infof("nameserver: handing over request for %s to filter responder: %s", q.ID(), conn.Reason)
|
||||||
m := new(dns.Msg)
|
return reply(responder)
|
||||||
m.SetReply(query)
|
|
||||||
m.Answer = rrCache.Answer
|
|
||||||
m.Ns = rrCache.Ns
|
|
||||||
m.Extra = rrCache.Extra
|
|
||||||
|
|
||||||
if err := writeDNSResponse(w, m); err != nil {
|
|
||||||
tracer.Warningf("nameserver: failed to return response %s%s to %s: %s", q.FQDN, q.QType, conn.Process(), err)
|
|
||||||
} else {
|
|
||||||
tracer.Debugf("nameserver: returning response %s%s to %s", q.FQDN, q.QType, conn.Process())
|
|
||||||
}
|
|
||||||
|
|
||||||
// save dns request as open
|
|
||||||
network.SaveOpenDNSRequest(conn)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeDNSResponse(w dns.ResponseWriter, m *dns.Msg) (err error) {
|
|
||||||
defer func() {
|
|
||||||
// recover from panic
|
|
||||||
if panicErr := recover(); panicErr != nil {
|
|
||||||
err = fmt.Errorf("panic: %s", panicErr)
|
|
||||||
log.Warningf("nameserver: panic caused by this msg: %#v", m)
|
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
err = w.WriteMsg(m)
|
// Request was blocked by the firewall.
|
||||||
return
|
switch conn.Verdict {
|
||||||
|
case network.VerdictBlock, network.VerdictDrop, network.VerdictFailed:
|
||||||
|
tracer.Infof("nameserver: request for %s from %s %s", q.ID(), conn.Process(), conn.Verdict.Verb())
|
||||||
|
return reply(conn, conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save dns request as open.
|
||||||
|
defer network.SaveOpenDNSRequest(conn)
|
||||||
|
|
||||||
|
// Reply with successful response.
|
||||||
|
tracer.Infof("nameserver: returning %s response %s to %s", conn.Verdict.Verb(), q.ID(), conn.Process())
|
||||||
|
return reply(rrCache, conn, rrCache)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
package nsutil
|
package nsutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
)
|
)
|
||||||
|
@ -13,35 +17,35 @@ import (
|
||||||
type Responder interface {
|
type Responder interface {
|
||||||
// ReplyWithDNS is called when a DNS response to a DNS message is
|
// ReplyWithDNS is called when a DNS response to a DNS message is
|
||||||
// crafted because the request is either denied or blocked.
|
// crafted because the request is either denied or blocked.
|
||||||
ReplyWithDNS(query *dns.Msg, reason string, reasonCtx interface{}) *dns.Msg
|
ReplyWithDNS(ctx context.Context, request *dns.Msg) *dns.Msg
|
||||||
}
|
}
|
||||||
|
|
||||||
// RRProvider defines the interface that any block/deny reason interface
|
// RRProvider defines the interface that any block/deny reason interface
|
||||||
// may implement to support adding additional DNS resource records to
|
// may implement to support adding additional DNS resource records to
|
||||||
// the DNS responses extra (additional) section.
|
// the DNS responses extra (additional) section.
|
||||||
type RRProvider interface {
|
type RRProvider interface {
|
||||||
// GetExtraRR is called when a DNS response to a DNS message is
|
// GetExtraRRs is called when a DNS response to a DNS message is
|
||||||
// crafted because the request is either denied or blocked.
|
// crafted because the request is either denied or blocked.
|
||||||
GetExtraRR(query *dns.Msg, reason string, reasonCtx interface{}) []dns.RR
|
GetExtraRRs(ctx context.Context, request *dns.Msg) []dns.RR
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResponderFunc is a convenience type to use a function
|
// ResponderFunc is a convenience type to use a function
|
||||||
// directly as a Responder.
|
// directly as a Responder.
|
||||||
type ResponderFunc func(query *dns.Msg, reason string, reasonCtx interface{}) *dns.Msg
|
type ResponderFunc func(ctx context.Context, request *dns.Msg) *dns.Msg
|
||||||
|
|
||||||
// ReplyWithDNS implements the Responder interface and calls rf.
|
// ReplyWithDNS implements the Responder interface and calls rf.
|
||||||
func (rf ResponderFunc) ReplyWithDNS(query *dns.Msg, reason string, reasonCtx interface{}) *dns.Msg {
|
func (rf ResponderFunc) ReplyWithDNS(ctx context.Context, request *dns.Msg) *dns.Msg {
|
||||||
return rf(query, reason, reasonCtx)
|
return rf(ctx, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ZeroIP is a ResponderFunc than replies with either 0.0.0.0 or :: for
|
// ZeroIP is a ResponderFunc than replies with either 0.0.0.0 or :: for
|
||||||
// each A or AAAA question respectively.
|
// each A or AAAA question respectively.
|
||||||
func ZeroIP() ResponderFunc {
|
func ZeroIP(msg string) ResponderFunc {
|
||||||
return func(query *dns.Msg, _ string, _ interface{}) *dns.Msg {
|
return func(ctx context.Context, request *dns.Msg) *dns.Msg {
|
||||||
m := new(dns.Msg)
|
reply := new(dns.Msg)
|
||||||
hasErr := false
|
hasErr := false
|
||||||
|
|
||||||
for _, question := range query.Question {
|
for _, question := range request.Question {
|
||||||
var rr dns.RR
|
var rr dns.RR
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
@ -53,40 +57,110 @@ func ZeroIP() ResponderFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("nameserver: failed to create zero-ip response for %s: %s", question.Name, err)
|
log.Tracer(ctx).Errorf("nameserver: failed to create zero-ip response for %s: %s", question.Name, err)
|
||||||
hasErr = true
|
hasErr = true
|
||||||
} else {
|
} else {
|
||||||
m.Answer = append(m.Answer, rr)
|
reply.Answer = append(reply.Answer, rr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasErr && len(m.Answer) == 0 {
|
switch {
|
||||||
m.SetRcode(query, dns.RcodeServerFailure)
|
case hasErr && len(reply.Answer) == 0:
|
||||||
} else {
|
reply.SetRcode(request, dns.RcodeServerFailure)
|
||||||
m.SetRcode(query, dns.RcodeSuccess)
|
case len(reply.Answer) == 0:
|
||||||
|
reply.SetRcode(request, dns.RcodeNameError)
|
||||||
|
default:
|
||||||
|
reply.SetRcode(request, dns.RcodeSuccess)
|
||||||
}
|
}
|
||||||
|
|
||||||
return m
|
AddMessageToReply(ctx, reply, log.InfoLevel, msg)
|
||||||
|
|
||||||
|
return reply
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Localhost(msg string) ResponderFunc {
|
||||||
|
return func(ctx context.Context, request *dns.Msg) *dns.Msg {
|
||||||
|
reply := new(dns.Msg)
|
||||||
|
hasErr := false
|
||||||
|
|
||||||
|
for _, question := range request.Question {
|
||||||
|
var rr dns.RR
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch question.Qtype {
|
||||||
|
case dns.TypeA:
|
||||||
|
rr, err = dns.NewRR("localhost. 0 IN A 127.0.0.1")
|
||||||
|
case dns.TypeAAAA:
|
||||||
|
rr, err = dns.NewRR("localhost. 0 IN AAAA ::1")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Tracer(ctx).Errorf("nameserver: failed to create localhost response for %s: %s", question.Name, err)
|
||||||
|
hasErr = true
|
||||||
|
} else {
|
||||||
|
reply.Answer = append(reply.Answer, rr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
AddMessageToReply(ctx, reply, log.InfoLevel, msg)
|
||||||
|
|
||||||
|
return reply
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NxDomain returns a ResponderFunc that replies with NXDOMAIN.
|
// NxDomain returns a ResponderFunc that replies with NXDOMAIN.
|
||||||
func NxDomain() ResponderFunc {
|
func NxDomain(msg string) ResponderFunc {
|
||||||
return func(query *dns.Msg, _ string, _ interface{}) *dns.Msg {
|
return func(ctx context.Context, request *dns.Msg) *dns.Msg {
|
||||||
return new(dns.Msg).SetRcode(query, dns.RcodeNameError)
|
reply := new(dns.Msg).SetRcode(request, dns.RcodeNameError)
|
||||||
|
AddMessageToReply(ctx, reply, log.InfoLevel, msg)
|
||||||
|
return reply
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refused returns a ResponderFunc that replies with REFUSED.
|
// Refused returns a ResponderFunc that replies with REFUSED.
|
||||||
func Refused() ResponderFunc {
|
func Refused(msg string) ResponderFunc {
|
||||||
return func(query *dns.Msg, _ string, _ interface{}) *dns.Msg {
|
return func(ctx context.Context, request *dns.Msg) *dns.Msg {
|
||||||
return new(dns.Msg).SetRcode(query, dns.RcodeRefused)
|
reply := new(dns.Msg).SetRcode(request, dns.RcodeRefused)
|
||||||
|
AddMessageToReply(ctx, reply, log.InfoLevel, msg)
|
||||||
|
return reply
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeFail returns a ResponderFunc that replies with SERVFAIL.
|
// ServerFailure returns a ResponderFunc that replies with SERVFAIL.
|
||||||
func ServeFail() ResponderFunc {
|
func ServerFailure(msg string) ResponderFunc {
|
||||||
return func(query *dns.Msg, _ string, _ interface{}) *dns.Msg {
|
return func(ctx context.Context, request *dns.Msg) *dns.Msg {
|
||||||
return new(dns.Msg).SetRcode(query, dns.RcodeServerFailure)
|
reply := new(dns.Msg).SetRcode(request, dns.RcodeServerFailure)
|
||||||
|
AddMessageToReply(ctx, reply, log.InfoLevel, msg)
|
||||||
|
return reply
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeMessageRecord(level log.Severity, msg string) (dns.RR, error) {
|
||||||
|
return dns.NewRR(fmt.Sprintf(
|
||||||
|
`%s.portmaster. 0 IN TXT "%s"`,
|
||||||
|
strings.ToLower(level.String()),
|
||||||
|
msg,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddMessageToReply(ctx context.Context, reply *dns.Msg, level log.Severity, msg string) {
|
||||||
|
if msg != "" {
|
||||||
|
rr, err := MakeMessageRecord(level, msg)
|
||||||
|
if err != nil {
|
||||||
|
log.Tracer(ctx).Warningf("nameserver: failed to add message to reply: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.Extra = append(reply.Extra, rr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +1,55 @@
|
||||||
package nameserver
|
package nameserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
"github.com/safing/portmaster/nameserver/nsutil"
|
"github.com/safing/portmaster/nameserver/nsutil"
|
||||||
"github.com/safing/portmaster/network"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// sendResponse sends a response to query using w. If reasonCtx is not
|
// sendResponse sends a response to query using w. The response message is
|
||||||
// nil and implements either the Responder or RRProvider interface then
|
// created by responder. If addExtraRRs is not nil and implements the
|
||||||
// those functions are used to craft a DNS response. If reasonCtx is nil
|
// RRProvider interface then it will be also used to add more RRs in the
|
||||||
// or does not implement the Responder interface and verdict is not set
|
// extra section.
|
||||||
// to failed a ZeroIP response will be sent. If verdict is set to failed
|
func sendResponse(
|
||||||
// then a ServFail will be sent instead.
|
ctx context.Context,
|
||||||
func sendResponse(w dns.ResponseWriter, query *dns.Msg, verdict network.Verdict, reason string, reasonCtx interface{}) {
|
w dns.ResponseWriter,
|
||||||
responder, ok := reasonCtx.(nsutil.Responder)
|
request *dns.Msg,
|
||||||
if !ok {
|
responder nsutil.Responder,
|
||||||
if verdict == network.VerdictFailed {
|
rrProviders ...nsutil.RRProvider,
|
||||||
responder = nsutil.ServeFail()
|
) error {
|
||||||
} else {
|
// Have the Responder craft a DNS reply.
|
||||||
responder = nsutil.ZeroIP()
|
reply := responder.ReplyWithDNS(ctx, request)
|
||||||
}
|
if reply == nil {
|
||||||
|
// Dropping query.
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
reply := responder.ReplyWithDNS(query, reason, reasonCtx)
|
// Add extra RRs through a custom RRProvider.
|
||||||
|
for _, rrProvider := range rrProviders {
|
||||||
if extra, ok := reasonCtx.(nsutil.RRProvider); ok {
|
rrs := rrProvider.GetExtraRRs(ctx, request)
|
||||||
rrs := extra.GetExtraRR(query, reason, reasonCtx)
|
|
||||||
reply.Extra = append(reply.Extra, rrs...)
|
reply.Extra = append(reply.Extra, rrs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write reply.
|
||||||
if err := writeDNSResponse(w, reply); err != nil {
|
if err := writeDNSResponse(w, reply); err != nil {
|
||||||
log.Errorf("nameserver: failed to send response: %s", err)
|
return fmt.Errorf("nameserver: failed to send response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeDNSResponse(w dns.ResponseWriter, m *dns.Msg) (err error) {
|
||||||
|
defer func() {
|
||||||
|
// recover from panic
|
||||||
|
if panicErr := recover(); panicErr != nil {
|
||||||
|
err = fmt.Errorf("panic: %s", panicErr)
|
||||||
|
log.Warningf("nameserver: panic caused by this msg: %#v", m)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = w.WriteMsg(m)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,14 @@ package network
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/safing/portbase/log"
|
||||||
|
"github.com/safing/portmaster/nameserver/nsutil"
|
||||||
"github.com/safing/portmaster/process"
|
"github.com/safing/portmaster/process"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -88,3 +92,48 @@ func writeOpenDNSRequestsToDB() {
|
||||||
conn.Unlock()
|
conn.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReplyWithDNS creates a new reply to the given request with the data from the RRCache, and additional informational records.
|
||||||
|
func (conn *Connection) ReplyWithDNS(ctx context.Context, request *dns.Msg) *dns.Msg {
|
||||||
|
// Select request responder.
|
||||||
|
switch conn.Verdict {
|
||||||
|
case VerdictBlock:
|
||||||
|
return nsutil.ZeroIP("").ReplyWithDNS(ctx, request)
|
||||||
|
case VerdictDrop:
|
||||||
|
return nil // Do not respond to request.
|
||||||
|
case VerdictFailed:
|
||||||
|
return nsutil.ZeroIP("").ReplyWithDNS(ctx, request)
|
||||||
|
default:
|
||||||
|
reply := nsutil.ServerFailure("").ReplyWithDNS(ctx, request)
|
||||||
|
nsutil.AddMessageToReply(ctx, reply, log.ErrorLevel, "INTERNAL ERROR: incorrect use of network.Connection's DNS Responder")
|
||||||
|
return reply
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExtraRRs returns a slice of RRs with additional informational records.
|
||||||
|
func (conn *Connection) GetExtraRRs(ctx context.Context, request *dns.Msg) []dns.RR {
|
||||||
|
// Select level to add the verdict record with.
|
||||||
|
var level log.Severity
|
||||||
|
switch conn.Verdict {
|
||||||
|
case VerdictFailed:
|
||||||
|
level = log.ErrorLevel
|
||||||
|
default:
|
||||||
|
level = log.InfoLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create resource record with verdict and reason.
|
||||||
|
rr, err := nsutil.MakeMessageRecord(level, fmt.Sprintf("%s: %s", conn.Verdict.Verb(), conn.Reason))
|
||||||
|
if err != nil {
|
||||||
|
log.Tracer(ctx).Warningf("filter: failed to add informational record to reply: %s", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
extra := []dns.RR{rr}
|
||||||
|
|
||||||
|
// Add additional records from ReasonContext.
|
||||||
|
if rrProvider, ok := conn.ReasonContext.(nsutil.RRProvider); ok {
|
||||||
|
rrs := rrProvider.GetExtraRRs(ctx, request)
|
||||||
|
extra = append(extra, rrs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return extra
|
||||||
|
}
|
||||||
|
|
|
@ -39,6 +39,30 @@ func (v Verdict) String() string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verb returns the verdict as a past tense verb.
|
||||||
|
func (v Verdict) Verb() string {
|
||||||
|
switch v {
|
||||||
|
case VerdictUndecided:
|
||||||
|
return "undecided"
|
||||||
|
case VerdictUndeterminable:
|
||||||
|
return "undeterminable"
|
||||||
|
case VerdictAccept:
|
||||||
|
return "accepted"
|
||||||
|
case VerdictBlock:
|
||||||
|
return "blocked"
|
||||||
|
case VerdictDrop:
|
||||||
|
return "dropped"
|
||||||
|
case VerdictRerouteToNameserver:
|
||||||
|
return "rerouted to nameserver"
|
||||||
|
case VerdictRerouteToTunnel:
|
||||||
|
return "rerouted to tunnel"
|
||||||
|
case VerdictFailed:
|
||||||
|
return "failed"
|
||||||
|
default:
|
||||||
|
return "invalid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Packer Directions
|
// Packer Directions
|
||||||
const (
|
const (
|
||||||
Inbound = true
|
Inbound = true
|
||||||
|
|
|
@ -28,6 +28,7 @@ type RRCache struct {
|
||||||
|
|
||||||
Server string // constant
|
Server string // constant
|
||||||
ServerScope int8 // constant
|
ServerScope int8 // constant
|
||||||
|
ServerInfo string // constant
|
||||||
|
|
||||||
servedFromCache bool // mutable
|
servedFromCache bool // mutable
|
||||||
requestingNew bool // mutable
|
requestingNew bool // mutable
|
||||||
|
@ -246,3 +247,61 @@ func (rrCache *RRCache) ShallowCopy() *RRCache {
|
||||||
FilteredEntries: rrCache.FilteredEntries,
|
FilteredEntries: rrCache.FilteredEntries,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReplyWithDNS creates a new reply to the given query with the data from the RRCache, and additional informational records.
|
||||||
|
func (rrCache *RRCache) ReplyWithDNS(ctx context.Context, request *dns.Msg) *dns.Msg {
|
||||||
|
// reply to query
|
||||||
|
reply := new(dns.Msg)
|
||||||
|
reply.SetRcode(request, dns.RcodeSuccess)
|
||||||
|
reply.Answer = rrCache.Answer
|
||||||
|
reply.Ns = rrCache.Ns
|
||||||
|
reply.Extra = rrCache.Extra
|
||||||
|
|
||||||
|
// Set NXDomain return code.
|
||||||
|
if rrCache.IsNXDomain() {
|
||||||
|
reply.Rcode = dns.RcodeNameError
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExtraRRs returns a slice of RRs with additional informational records.
|
||||||
|
func (rrCache *RRCache) GetExtraRRs(ctx context.Context, query *dns.Msg) (extra []dns.RR) {
|
||||||
|
// Add cache status and source of data.
|
||||||
|
if rrCache.servedFromCache {
|
||||||
|
extra = addExtra(ctx, extra, log.InfoLevel, "served from cache, resolved by "+rrCache.ServerInfo)
|
||||||
|
} else {
|
||||||
|
extra = addExtra(ctx, extra, log.InfoLevel, "freshly resolved by "+rrCache.ServerInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add expiry and cache information.
|
||||||
|
if rrCache.Expired() {
|
||||||
|
extra = addExtra(ctx, extra, log.InfoLevel, fmt.Sprintf("record expired since %s, requesting new", time.Since(time.Unix(rrCache.TTL, 0))))
|
||||||
|
} else {
|
||||||
|
extra = addExtra(ctx, extra, log.InfoLevel, fmt.Sprintf("record valid for %s", time.Until(time.Unix(rrCache.TTL, 0))))
|
||||||
|
}
|
||||||
|
if rrCache.requestingNew {
|
||||||
|
extra = addExtra(ctx, extra, log.InfoLevel, "async request to refresh the cache has been started")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add information about filtered entries.
|
||||||
|
if rrCache.Filtered {
|
||||||
|
if len(rrCache.FilteredEntries) > 1 {
|
||||||
|
extra = addExtra(ctx, extra, log.InfoLevel, fmt.Sprintf("%d records have been filtered", len(rrCache.FilteredEntries)))
|
||||||
|
} else {
|
||||||
|
extra = addExtra(ctx, extra, log.InfoLevel, fmt.Sprintf("%d record has been filtered", len(rrCache.FilteredEntries)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return extra
|
||||||
|
}
|
||||||
|
|
||||||
|
func addExtra(ctx context.Context, extra []dns.RR, level log.Severity, msg string) []dns.RR {
|
||||||
|
rr, err := nsutil.MakeMessageRecord(level, msg)
|
||||||
|
if err != nil {
|
||||||
|
log.Tracer(ctx).Warningf("resolver: failed to add informational record to reply: %s", err)
|
||||||
|
return extra
|
||||||
|
}
|
||||||
|
extra = append(extra, rr)
|
||||||
|
return extra
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue