mirror of
https://github.com/safing/portmaster
synced 2025-09-02 02:29:12 +00:00
Merge pull request #94 from safing/feature/connectivity-domains
Improve captive portal handling
This commit is contained in:
commit
d1d43b9648
17 changed files with 333 additions and 36 deletions
|
@ -7,6 +7,8 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/safing/portmaster/netenv"
|
||||||
|
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
"github.com/safing/portmaster/network"
|
"github.com/safing/portmaster/network"
|
||||||
"github.com/safing/portmaster/network/netutils"
|
"github.com/safing/portmaster/network/netutils"
|
||||||
|
@ -50,6 +52,7 @@ func DecideOnConnection(ctx context.Context, conn *network.Connection, pkt packe
|
||||||
checkSelfCommunication,
|
checkSelfCommunication,
|
||||||
checkProfileExists,
|
checkProfileExists,
|
||||||
checkConnectionType,
|
checkConnectionType,
|
||||||
|
checkCaptivePortal,
|
||||||
checkConnectionScope,
|
checkConnectionScope,
|
||||||
checkEndpointLists,
|
checkEndpointLists,
|
||||||
checkBypassPrevention,
|
checkBypassPrevention,
|
||||||
|
@ -178,6 +181,16 @@ func checkConnectionType(ctx context.Context, conn *network.Connection, _ packet
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkCaptivePortal(_ context.Context, conn *network.Connection, _ packet.Packet) bool {
|
||||||
|
if netenv.GetOnlineStatus() == netenv.StatusPortal &&
|
||||||
|
conn.Entity.Domain == netenv.GetCaptivePortal().Domain {
|
||||||
|
conn.Accept("captive portal access permitted")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func checkConnectionScope(_ context.Context, conn *network.Connection, _ packet.Packet) bool {
|
func checkConnectionScope(_ context.Context, conn *network.Connection, _ packet.Packet) bool {
|
||||||
p := conn.Process().Profile()
|
p := conn.Process().Profile()
|
||||||
|
|
||||||
|
|
|
@ -115,7 +115,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er
|
||||||
|
|
||||||
// return with server failure if offline
|
// return with server failure if offline
|
||||||
if netenv.GetOnlineStatus() == netenv.StatusOffline &&
|
if netenv.GetOnlineStatus() == netenv.StatusOffline &&
|
||||||
!netenv.IsOnlineStatusTestDomain(q.FQDN) {
|
!netenv.IsConnectivityDomain(q.FQDN) {
|
||||||
log.Tracer(ctx).Debugf("resolver: not resolving %s, device is offline", q.FQDN)
|
log.Tracer(ctx).Debugf("resolver: not resolving %s, device is offline", q.FQDN)
|
||||||
returnServerFailure(w, query)
|
returnServerFailure(w, query)
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -6,7 +6,7 @@ func Nameservers() []Nameserver {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Gateways() []*net.IP {
|
func Gateways() []net.IP {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
gateways = make([]*net.IP, 0)
|
gateways = make([]net.IP, 0)
|
||||||
gatewaysLock sync.Mutex
|
gatewaysLock sync.Mutex
|
||||||
gatewaysExpires = time.Now()
|
gatewaysExpires = time.Now()
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Gateways returns the currently active gateways.
|
// Gateways returns the currently active gateways.
|
||||||
func Gateways() []*net.IP {
|
func Gateways() []net.IP {
|
||||||
// locking
|
// locking
|
||||||
gatewaysLock.Lock()
|
gatewaysLock.Lock()
|
||||||
defer gatewaysLock.Unlock()
|
defer gatewaysLock.Unlock()
|
||||||
|
@ -45,14 +45,14 @@ func Gateways() []*net.IP {
|
||||||
}()
|
}()
|
||||||
// logic
|
// logic
|
||||||
|
|
||||||
newGateways := make([]*net.IP, 0)
|
gateways = make([]net.IP, 0)
|
||||||
var decoded []byte
|
var decoded []byte
|
||||||
|
|
||||||
// open file
|
// open file
|
||||||
route, err := os.Open("/proc/net/route")
|
route, err := os.Open("/proc/net/route")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warningf("environment: could not read /proc/net/route: %s", err)
|
log.Warningf("environment: could not read /proc/net/route: %s", err)
|
||||||
return newGateways
|
return gateways
|
||||||
}
|
}
|
||||||
defer route.Close()
|
defer route.Close()
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ func Gateways() []*net.IP {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
gate := net.IPv4(decoded[3], decoded[2], decoded[1], decoded[0])
|
gate := net.IPv4(decoded[3], decoded[2], decoded[1], decoded[0])
|
||||||
newGateways = append(newGateways, &gate)
|
gateways = append(gateways, gate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ func Gateways() []*net.IP {
|
||||||
v6route, err := os.Open("/proc/net/ipv6_route")
|
v6route, err := os.Open("/proc/net/ipv6_route")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warningf("environment: could not read /proc/net/ipv6_route: %s", err)
|
log.Warningf("environment: could not read /proc/net/ipv6_route: %s", err)
|
||||||
return newGateways
|
return gateways
|
||||||
}
|
}
|
||||||
defer v6route.Close()
|
defer v6route.Close()
|
||||||
|
|
||||||
|
@ -110,11 +110,11 @@ func Gateways() []*net.IP {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
gate := net.IP(decoded)
|
gate := net.IP(decoded)
|
||||||
newGateways = append(newGateways, &gate)
|
gateways = append(gateways, gate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return newGateways
|
return gateways
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nameservers returns the currently active nameservers.
|
// Nameservers returns the currently active nameservers.
|
||||||
|
|
|
@ -1,13 +1,95 @@
|
||||||
package netenv
|
package netenv
|
||||||
|
|
||||||
import "net"
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
nameserversRecheck = 2 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
nameservers = make([]Nameserver, 0)
|
||||||
|
nameserversLock sync.Mutex
|
||||||
|
nameserversExpires = time.Now()
|
||||||
|
)
|
||||||
|
|
||||||
// Nameservers returns the currently active nameservers.
|
// Nameservers returns the currently active nameservers.
|
||||||
func Nameservers() []Nameserver {
|
func Nameservers() []Nameserver {
|
||||||
|
// locking
|
||||||
|
nameserversLock.Lock()
|
||||||
|
defer nameserversLock.Unlock()
|
||||||
|
// cache
|
||||||
|
if nameserversExpires.After(time.Now()) {
|
||||||
|
return nameservers
|
||||||
|
}
|
||||||
|
// update cache expiry when finished
|
||||||
|
defer func() {
|
||||||
|
nameserversExpires = time.Now().Add(nameserversRecheck)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// reset
|
||||||
|
nameservers = make([]Nameserver, 0)
|
||||||
|
|
||||||
|
// This is a preliminary workaround until we have more time for proper interface using iphlpapi.dll
|
||||||
|
// TODO: make nice implementation
|
||||||
|
|
||||||
|
var output = make(chan []byte, 1)
|
||||||
|
module.StartWorker("get assigned nameservers", func(ctx context.Context) error {
|
||||||
|
cmd := exec.CommandContext(ctx, "nslookup", "localhost")
|
||||||
|
data, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("netenv: failed to get assigned nameserves: %s", err)
|
||||||
|
output <- nil
|
||||||
|
} else {
|
||||||
|
output <- data
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
select {
|
||||||
|
case data := <-output:
|
||||||
|
scanner := bufio.NewScanner(bytes.NewReader(data))
|
||||||
|
scanner.Split(bufio.ScanLines)
|
||||||
|
for scanner.Scan() {
|
||||||
|
// check if we found the correct line
|
||||||
|
if !strings.HasPrefix(scanner.Text(), "Address:") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// split into fields, check if we have enough fields
|
||||||
|
fields := strings.Fields(scanner.Text())
|
||||||
|
if len(fields) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// parse nameserver, return if valid IP found
|
||||||
|
ns := net.ParseIP(fields[1])
|
||||||
|
if ns != nil {
|
||||||
|
nameservers = append(nameservers, Nameserver{
|
||||||
|
IP: ns,
|
||||||
|
})
|
||||||
|
return nameservers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Debug("netenv: could not find assigned nameserver")
|
||||||
|
return nameservers
|
||||||
|
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
log.Debug("netenv: timed out while getting assigned nameserves")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nameservers
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gateways returns the currently active gateways.
|
// Gateways returns the currently active gateways.
|
||||||
func Gateways() []*net.IP {
|
func Gateways() []net.IP {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,8 @@ const (
|
||||||
ResolverTestFqdn = "one.one.one.one."
|
ResolverTestFqdn = "one.one.one.one."
|
||||||
ResolverTestRRType = dns.TypeA
|
ResolverTestRRType = dns.TypeA
|
||||||
ResolverTestExpectedResponse = "1.1.1.1"
|
ResolverTestExpectedResponse = "1.1.1.1"
|
||||||
|
|
||||||
|
SpecialCaptivePortalDomain = "captiveportal.local."
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -62,12 +64,12 @@ func init() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsOnlineStatusTestDomain checks whether the given fqdn is used for testing online status.
|
// IsConnectivityDomain checks whether the given domain (fqdn) is used for any connectivity related network connections and should always be resolved using the network assigned DNS server.
|
||||||
func IsOnlineStatusTestDomain(domain string) bool {
|
func IsConnectivityDomain(domain string) bool {
|
||||||
switch domain {
|
switch domain {
|
||||||
case "detectportal.firefox.com.":
|
case "detectportal.firefox.com.",
|
||||||
return true
|
"one.one.one.one.",
|
||||||
case "one.one.one.one.":
|
GetCaptivePortal().Domain:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,10 +106,17 @@ var (
|
||||||
onlineStatusInvestigationInProgress = abool.NewBool(false)
|
onlineStatusInvestigationInProgress = abool.NewBool(false)
|
||||||
onlineStatusInvestigationWg sync.WaitGroup
|
onlineStatusInvestigationWg sync.WaitGroup
|
||||||
|
|
||||||
captivePortalURL string
|
captivePortal = &CaptivePortal{}
|
||||||
captivePortalLock sync.Mutex
|
captivePortalLock sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CaptivePortal holds information about a detected captive portal.
|
||||||
|
type CaptivePortal struct {
|
||||||
|
URL string
|
||||||
|
Domain string
|
||||||
|
IP net.IP
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
var onlineStatusValue int32
|
var onlineStatusValue int32
|
||||||
onlineStatus = &onlineStatusValue
|
onlineStatus = &onlineStatusValue
|
||||||
|
@ -147,18 +156,16 @@ func updateOnlineStatus(status OnlineStatus, portalURL, comment string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// captive portal
|
// captive portal
|
||||||
captivePortalLock.Lock()
|
// delete if offline, update only if there is a new value
|
||||||
defer captivePortalLock.Unlock()
|
if status == StatusOffline || portalURL != "" {
|
||||||
if portalURL != captivePortalURL {
|
setCaptivePortal(portalURL)
|
||||||
captivePortalURL = portalURL
|
|
||||||
changed = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// trigger event
|
// trigger event
|
||||||
if changed {
|
if changed {
|
||||||
module.TriggerEvent(OnlineStatusChangedEvent, nil)
|
module.TriggerEvent(OnlineStatusChangedEvent, nil)
|
||||||
if status == StatusPortal {
|
if status == StatusPortal {
|
||||||
log.Infof(`network: setting online status to %s at "%s" (%s)`, status, captivePortalURL, comment)
|
log.Infof(`network: setting online status to %s at "%s" (%s)`, status, portalURL, comment)
|
||||||
} else {
|
} else {
|
||||||
log.Infof("network: setting online status to %s (%s)", status, comment)
|
log.Infof("network: setting online status to %s (%s)", status, comment)
|
||||||
}
|
}
|
||||||
|
@ -166,12 +173,56 @@ func updateOnlineStatus(status OnlineStatus, portalURL, comment string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCaptivePortalURL returns the current captive portal url as a string.
|
func setCaptivePortal(portalURL string) {
|
||||||
func GetCaptivePortalURL() string {
|
|
||||||
captivePortalLock.Lock()
|
captivePortalLock.Lock()
|
||||||
defer captivePortalLock.Unlock()
|
defer captivePortalLock.Unlock()
|
||||||
|
|
||||||
return captivePortalURL
|
// delete
|
||||||
|
if portalURL == "" {
|
||||||
|
captivePortal = &CaptivePortal{}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// set
|
||||||
|
captivePortal = &CaptivePortal{
|
||||||
|
URL: portalURL,
|
||||||
|
}
|
||||||
|
parsedURL, err := url.Parse(portalURL)
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
log.Debugf(`netenv: failed to parse captive portal URL "%s": %s`, portalURL, err)
|
||||||
|
return
|
||||||
|
case parsedURL.Hostname() == "":
|
||||||
|
log.Debugf(`netenv: captive portal URL "%s" has no domain or IP`, portalURL)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
// try to parse an IP
|
||||||
|
portalIP := net.ParseIP(parsedURL.Hostname())
|
||||||
|
if portalIP != nil {
|
||||||
|
captivePortal.IP = portalIP
|
||||||
|
captivePortal.Domain = SpecialCaptivePortalDomain
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to parse domain
|
||||||
|
// ensure fqdn format
|
||||||
|
domain := dns.Fqdn(parsedURL.Hostname())
|
||||||
|
// check validity
|
||||||
|
if !netutils.IsValidFqdn(domain) {
|
||||||
|
log.Debugf(`netenv: captive portal domain/IP "%s" is invalid`, parsedURL.Hostname())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// set domain
|
||||||
|
captivePortal.Domain = domain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCaptivePortal returns the current captive portal. The returned struct must not be edited.
|
||||||
|
func GetCaptivePortal() *CaptivePortal {
|
||||||
|
captivePortalLock.Lock()
|
||||||
|
defer captivePortalLock.Unlock()
|
||||||
|
|
||||||
|
return captivePortal
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReportSuccessfulConnection hints the online status monitoring system that a connection attempt was successful.
|
// ReportSuccessfulConnection hints the online status monitoring system that a connection attempt was successful.
|
||||||
|
|
|
@ -8,5 +8,5 @@ import (
|
||||||
func TestCheckOnlineStatus(t *testing.T) {
|
func TestCheckOnlineStatus(t *testing.T) {
|
||||||
checkOnlineStatus(context.Background())
|
checkOnlineStatus(context.Background())
|
||||||
t.Logf("online status: %s", GetOnlineStatus())
|
t.Logf("online status: %s", GetOnlineStatus())
|
||||||
t.Logf("captive portal: %s", GetCaptivePortalURL())
|
t.Logf("captive portal: %+v", GetCaptivePortal())
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/safing/portmaster/netenv"
|
||||||
|
|
||||||
"github.com/safing/portbase/database/record"
|
"github.com/safing/portbase/database/record"
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
"github.com/safing/portmaster/intel"
|
"github.com/safing/portmaster/intel"
|
||||||
|
@ -148,6 +150,13 @@ 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) {
|
||||||
|
scope = portal.Domain
|
||||||
|
entity.Domain = portal.Domain
|
||||||
|
}
|
||||||
|
|
||||||
if scope == "" {
|
if scope == "" {
|
||||||
|
|
||||||
// outbound direct (possibly P2P) connection
|
// outbound direct (possibly P2P) connection
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package netutils
|
package netutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
@ -56,3 +58,24 @@ func IsValidFqdn(fqdn string) bool {
|
||||||
_, ok := dns.IsDomainName(fqdn)
|
_, ok := dns.IsDomainName(fqdn)
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IPsToRRs transforms the given IPs to resource records.
|
||||||
|
func IPsToRRs(domain string, ips []net.IP) ([]dns.RR, error) {
|
||||||
|
records := make([]dns.RR, 0, len(ips))
|
||||||
|
var rr dns.RR
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for _, ip := range ips {
|
||||||
|
if ip.To4() != nil {
|
||||||
|
rr, err = dns.NewRR(fmt.Sprintf("%s 17 IN A %s", domain, ip))
|
||||||
|
} else {
|
||||||
|
rr, err = dns.NewRR(fmt.Sprintf("%s 17 IN AAAA %s", domain, ip))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create record for %s: %w", ip, err)
|
||||||
|
}
|
||||||
|
records = append(records, rr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return records, nil
|
||||||
|
}
|
|
@ -24,6 +24,10 @@ func init() {
|
||||||
func prep() error {
|
func prep() error {
|
||||||
intel.SetReverseResolver(ResolveIPAndValidate)
|
intel.SetReverseResolver(ResolveIPAndValidate)
|
||||||
|
|
||||||
|
if err := prepEnvResolver(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return prepConfig()
|
return prepConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ var (
|
||||||
// basic errors
|
// basic errors
|
||||||
|
|
||||||
// ErrNotFound is a basic error that will match all "not found" errors
|
// ErrNotFound is a basic error that will match all "not found" errors
|
||||||
ErrNotFound = errors.New("record does not exist")
|
ErrNotFound = errors.New("record could not be found")
|
||||||
// ErrBlocked is basic error that will match all "blocked" errors
|
// ErrBlocked is basic error that will match all "blocked" errors
|
||||||
ErrBlocked = errors.New("query was blocked")
|
ErrBlocked = errors.New("query was blocked")
|
||||||
// ErrLocalhost is returned to *.localhost queries
|
// ErrLocalhost is returned to *.localhost queries
|
||||||
|
@ -30,6 +30,8 @@ var (
|
||||||
ErrOffline = errors.New("device is offine")
|
ErrOffline = errors.New("device is offine")
|
||||||
// ErrFailure is returned when the type of failure is unclear
|
// ErrFailure is returned when the type of failure is unclear
|
||||||
ErrFailure = errors.New("query failed")
|
ErrFailure = errors.New("query failed")
|
||||||
|
// ErrContinue is returned when the resolver has no answer, and the next resolver should be asked
|
||||||
|
ErrContinue = errors.New("resolver has no answer")
|
||||||
|
|
||||||
// detailed errors
|
// detailed errors
|
||||||
|
|
||||||
|
@ -228,7 +230,7 @@ func resolveAndCache(ctx context.Context, q *Query) (rrCache *RRCache, err error
|
||||||
|
|
||||||
// check if we are online
|
// check if we are online
|
||||||
if netenv.GetOnlineStatus() == netenv.StatusOffline {
|
if netenv.GetOnlineStatus() == netenv.StatusOffline {
|
||||||
if !netenv.IsOnlineStatusTestDomain(q.FQDN) {
|
if !netenv.IsConnectivityDomain(q.FQDN) {
|
||||||
log.Tracer(ctx).Debugf("resolver: not resolving %s, device is offline", q.FQDN)
|
log.Tracer(ctx).Debugf("resolver: not resolving %s, device is offline", q.FQDN)
|
||||||
// we are offline and this is not an online check query
|
// we are offline and this is not an online check query
|
||||||
return nil, ErrOffline
|
return nil, ErrOffline
|
||||||
|
@ -259,8 +261,10 @@ resolveLoop:
|
||||||
case errors.Is(err, ErrBlocked):
|
case errors.Is(err, ErrBlocked):
|
||||||
// some resolvers might also block
|
// some resolvers might also block
|
||||||
return nil, err
|
return nil, err
|
||||||
|
case errors.Is(err, ErrContinue):
|
||||||
|
continue
|
||||||
case netenv.GetOnlineStatus() == netenv.StatusOffline &&
|
case netenv.GetOnlineStatus() == netenv.StatusOffline &&
|
||||||
!netenv.IsOnlineStatusTestDomain(q.FQDN):
|
!netenv.IsConnectivityDomain(q.FQDN):
|
||||||
log.Tracer(ctx).Debugf("resolver: not resolving %s, device is offline", q.FQDN)
|
log.Tracer(ctx).Debugf("resolver: not resolving %s, device is offline", q.FQDN)
|
||||||
// we are offline and this is not an online check query
|
// we are offline and this is not an online check query
|
||||||
return nil, ErrOffline
|
return nil, ErrOffline
|
||||||
|
|
89
resolver/resolver-env.go
Normal file
89
resolver/resolver-env.go
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
package resolver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/safing/portbase/log"
|
||||||
|
"github.com/safing/portmaster/netenv"
|
||||||
|
"github.com/safing/portmaster/network/netutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
envResolver = &Resolver{
|
||||||
|
Server: ServerSourceEnv,
|
||||||
|
ServerType: ServerTypeEnv,
|
||||||
|
ServerIPScope: netutils.SiteLocal,
|
||||||
|
Source: ServerSourceEnv,
|
||||||
|
Conn: &envResolverConn{},
|
||||||
|
}
|
||||||
|
|
||||||
|
localSOA dns.RR
|
||||||
|
)
|
||||||
|
|
||||||
|
func prepEnvResolver() (err error) {
|
||||||
|
localSOA, err = dns.NewRR("local. 17 IN SOA localhost. none.localhost. 17 17 17 17 17")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type envResolverConn struct{}
|
||||||
|
|
||||||
|
func (er *envResolverConn) Query(ctx context.Context, q *Query) (*RRCache, error) {
|
||||||
|
// prepping
|
||||||
|
portal := netenv.GetCaptivePortal()
|
||||||
|
|
||||||
|
// check for matching name
|
||||||
|
switch q.FQDN {
|
||||||
|
case "local.":
|
||||||
|
// Firefox requests the SOA request for local. before resolving any local. domains.
|
||||||
|
// Others might be doing this too. We guessed this behaviour, weren't able to find docs.
|
||||||
|
if q.QType == dns.Type(dns.TypeSOA) {
|
||||||
|
return er.makeRRCache(q, []dns.RR{localSOA}), nil
|
||||||
|
}
|
||||||
|
return nil, ErrNotFound
|
||||||
|
|
||||||
|
case netenv.SpecialCaptivePortalDomain:
|
||||||
|
if portal.IP != nil {
|
||||||
|
records, err := netutils.IPsToRRs(q.FQDN, []net.IP{portal.IP})
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("nameserver: failed to create captive portal response to %s: %s", q.FQDN, err)
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return er.makeRRCache(q, records), nil
|
||||||
|
}
|
||||||
|
return nil, ErrNotFound
|
||||||
|
|
||||||
|
case "router.local.":
|
||||||
|
routers := netenv.Gateways()
|
||||||
|
if len(routers) == 0 {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
records, err := netutils.IPsToRRs(q.FQDN, routers)
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("nameserver: failed to create gateway response to %s: %s", q.FQDN, err)
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return er.makeRRCache(q, records), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// no match
|
||||||
|
return nil, ErrContinue // continue with next resolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func (er *envResolverConn) makeRRCache(q *Query, answers []dns.RR) *RRCache {
|
||||||
|
q.NoCaching = true // disable caching, as the env always has the data available and more up to date.
|
||||||
|
return &RRCache{
|
||||||
|
Domain: q.FQDN,
|
||||||
|
Question: q.QType,
|
||||||
|
Answer: answers,
|
||||||
|
Server: envResolver.Server,
|
||||||
|
ServerScope: envResolver.ServerIPScope,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (er *envResolverConn) ReportFailure() {}
|
||||||
|
|
||||||
|
func (er *envResolverConn) IsFailing() bool {
|
||||||
|
return false
|
||||||
|
}
|
|
@ -18,10 +18,12 @@ const (
|
||||||
ServerTypeTCP = "tcp"
|
ServerTypeTCP = "tcp"
|
||||||
ServerTypeDoT = "dot"
|
ServerTypeDoT = "dot"
|
||||||
ServerTypeDoH = "doh"
|
ServerTypeDoH = "doh"
|
||||||
|
ServerTypeEnv = "env"
|
||||||
|
|
||||||
ServerSourceConfigured = "config"
|
ServerSourceConfigured = "config"
|
||||||
ServerSourceAssigned = "dhcp"
|
ServerSourceAssigned = "dhcp"
|
||||||
ServerSourceMDNS = "mdns"
|
ServerSourceMDNS = "mdns"
|
||||||
|
ServerSourceEnv = "env"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -24,6 +24,7 @@ type Scope struct {
|
||||||
var (
|
var (
|
||||||
globalResolvers []*Resolver // all (global) resolvers
|
globalResolvers []*Resolver // all (global) resolvers
|
||||||
localResolvers []*Resolver // all resolvers that are in site-local or link-local IP ranges
|
localResolvers []*Resolver // all resolvers that are in site-local or link-local IP ranges
|
||||||
|
systemResolvers []*Resolver // all resolvers that were assigned by the system
|
||||||
localScopes []*Scope // list of scopes with a list of local resolvers that can resolve the scope
|
localScopes []*Scope // list of scopes with a list of local resolvers that can resolve the scope
|
||||||
activeResolvers map[string]*Resolver // lookup map of all resolvers
|
activeResolvers map[string]*Resolver // lookup map of all resolvers
|
||||||
resolversLock sync.RWMutex
|
resolversLock sync.RWMutex
|
||||||
|
@ -231,7 +232,7 @@ func loadResolvers() {
|
||||||
globalResolvers = newResolvers
|
globalResolvers = newResolvers
|
||||||
|
|
||||||
// assing resolvers to scopes
|
// assing resolvers to scopes
|
||||||
setLocalAndScopeResolvers(globalResolvers)
|
setScopedResolvers(globalResolvers)
|
||||||
|
|
||||||
// set active resolvers (for cache validation)
|
// set active resolvers (for cache validation)
|
||||||
// reset
|
// reset
|
||||||
|
@ -241,6 +242,7 @@ func loadResolvers() {
|
||||||
activeResolvers[resolver.Server] = resolver
|
activeResolvers[resolver.Server] = resolver
|
||||||
}
|
}
|
||||||
activeResolvers[mDNSResolver.Server] = mDNSResolver
|
activeResolvers[mDNSResolver.Server] = mDNSResolver
|
||||||
|
activeResolvers[envResolver.Server] = envResolver
|
||||||
|
|
||||||
// log global resolvers
|
// log global resolvers
|
||||||
if len(globalResolvers) > 0 {
|
if len(globalResolvers) > 0 {
|
||||||
|
@ -282,9 +284,10 @@ func loadResolvers() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setLocalAndScopeResolvers(resolvers []*Resolver) {
|
func setScopedResolvers(resolvers []*Resolver) {
|
||||||
// make list with local resolvers
|
// make list with local resolvers
|
||||||
localResolvers = make([]*Resolver, 0)
|
localResolvers = make([]*Resolver, 0)
|
||||||
|
systemResolvers = make([]*Resolver, 0)
|
||||||
localScopes = make([]*Scope, 0)
|
localScopes = make([]*Scope, 0)
|
||||||
|
|
||||||
for _, resolver := range resolvers {
|
for _, resolver := range resolvers {
|
||||||
|
@ -292,6 +295,10 @@ func setLocalAndScopeResolvers(resolvers []*Resolver) {
|
||||||
localResolvers = append(localResolvers, resolver)
|
localResolvers = append(localResolvers, resolver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if resolver.Source == "dhcp" {
|
||||||
|
systemResolvers = append(systemResolvers, resolver)
|
||||||
|
}
|
||||||
|
|
||||||
if resolver.Search != nil {
|
if resolver.Search != nil {
|
||||||
// add resolver to custom searches
|
// add resolver to custom searches
|
||||||
for _, search := range resolver.Search {
|
for _, search := range resolver.Search {
|
||||||
|
|
|
@ -32,6 +32,6 @@ func TestResolveIPAndValidate(t *testing.T) {
|
||||||
testReverse(t, "1.1.1.1", "one.one.one.one.", "")
|
testReverse(t, "1.1.1.1", "one.one.one.one.", "")
|
||||||
testReverse(t, "2606:4700:4700::1111", "one.one.one.one.", "")
|
testReverse(t, "2606:4700:4700::1111", "one.one.one.one.", "")
|
||||||
|
|
||||||
testReverse(t, "93.184.216.34", "example.com.", "record does not exist: 34.216.184.93.in-addr.arpa.PTR")
|
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 does not exist: 153.109.199.185.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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/safing/portmaster/netenv"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
)
|
)
|
||||||
|
@ -124,6 +126,13 @@ func GetResolversInScope(ctx context.Context, q *Query) (selected []*Resolver) {
|
||||||
// global -> local scopes, global
|
// global -> local scopes, global
|
||||||
// special -> local scopes, local
|
// special -> local scopes, local
|
||||||
|
|
||||||
|
// special connectivity domains
|
||||||
|
if netenv.IsConnectivityDomain(q.FQDN) && len(systemResolvers) > 0 {
|
||||||
|
selected = append(selected, envResolver)
|
||||||
|
selected = append(selected, systemResolvers...) // dhcp assigned resolvers
|
||||||
|
return selected
|
||||||
|
}
|
||||||
|
|
||||||
// check local scopes
|
// check local scopes
|
||||||
for _, scope := range localScopes {
|
for _, scope := range localScopes {
|
||||||
if strings.HasSuffix(q.dotPrefixedFQDN, scope.Domain) {
|
if strings.HasSuffix(q.dotPrefixedFQDN, scope.Domain) {
|
||||||
|
@ -169,6 +178,8 @@ func GetResolversInScope(ctx context.Context, q *Query) (selected []*Resolver) {
|
||||||
|
|
||||||
// check for .local mdns
|
// check for .local mdns
|
||||||
if strings.HasSuffix(q.dotPrefixedFQDN, local) {
|
if strings.HasSuffix(q.dotPrefixedFQDN, local) {
|
||||||
|
// add env resolver
|
||||||
|
selected = append(selected, envResolver)
|
||||||
// add mdns
|
// add mdns
|
||||||
if err := mDNSResolver.checkCompliance(ctx, q); err == nil {
|
if err := mDNSResolver.checkCompliance(ctx, q); err == nil {
|
||||||
selected = append(selected, mDNSResolver)
|
selected = append(selected, mDNSResolver)
|
||||||
|
@ -255,6 +266,8 @@ func (resolver *Resolver) checkCompliance(_ context.Context, q *Query) error {
|
||||||
// compliant
|
// compliant
|
||||||
case ServerTypeDoH:
|
case ServerTypeDoH:
|
||||||
// compliant
|
// compliant
|
||||||
|
case ServerTypeEnv:
|
||||||
|
// compliant (data is sources from local network only and is highly limited)
|
||||||
default:
|
default:
|
||||||
return errInsecureProtocol
|
return errInsecureProtocol
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue