Add match domains config

This commit is contained in:
Owen
2026-07-15 15:51:13 -04:00
parent b39be4f5b0
commit 9513433c07
7 changed files with 161 additions and 3 deletions

View File

@@ -29,6 +29,7 @@ type ConnectionRequest struct {
PingInterval string `json:"pingInterval,omitempty"`
PingTimeout string `json:"pingTimeout,omitempty"`
OrgID string `json:"orgId,omitempty"`
MatchDomains []string `json:"matchDomains,omitempty"`
}
// SwitchOrgRequest defines the structure for switching organizations

View File

@@ -27,6 +27,13 @@ type OlmConfig struct {
UpstreamDNS []string `json:"upstreamDNS"`
InterfaceName string `json:"interface"`
// MatchDomains lists FQDN wildcard patterns (using * and ? wildcards, e.g.
// "*.proxy.internal") that olm should check against local records / resolve
// via UpstreamDNS. Queries for domains that don't match any pattern are sent
// directly to the host's own system DNS servers instead. Empty means match
// every domain (i.e. the feature is disabled).
MatchDomains []string `json:"matchDomains"`
// Logging
LogLevel string `json:"logLevel"`
@@ -99,6 +106,7 @@ func DefaultConfig() *OlmConfig {
config.sources["mtu"] = string(SourceDefault)
config.sources["dns"] = string(SourceDefault)
config.sources["upstreamDNS"] = string(SourceDefault)
config.sources["matchDomains"] = string(SourceDefault)
config.sources["logLevel"] = string(SourceDefault)
config.sources["interface"] = string(SourceDefault)
config.sources["enableApi"] = string(SourceDefault)
@@ -229,6 +237,10 @@ func loadConfigFromEnv(config *OlmConfig) {
config.UpstreamDNS = []string{val}
config.sources["upstreamDNS"] = string(SourceEnv)
}
if val := os.Getenv("MATCH_DOMAINS"); val != "" {
config.MatchDomains = splitComma(val)
config.sources["matchDomains"] = string(SourceEnv)
}
if val := os.Getenv("LOG_LEVEL"); val != "" {
config.LogLevel = val
config.sources["logLevel"] = string(SourceEnv)
@@ -293,6 +305,7 @@ func loadConfigFromCLI(config *OlmConfig, args []string) (bool, bool, error) {
"mtu": config.MTU,
"dns": config.DNS,
"upstreamDNS": fmt.Sprintf("%v", config.UpstreamDNS),
"matchDomains": fmt.Sprintf("%v", config.MatchDomains),
"logLevel": config.LogLevel,
"interface": config.InterfaceName,
"httpAddr": config.HTTPAddr,
@@ -317,6 +330,8 @@ func loadConfigFromCLI(config *OlmConfig, args []string) (bool, bool, error) {
serviceFlags.StringVar(&config.DNS, "dns", config.DNS, "DNS server to use")
var upstreamDNSFlag string
serviceFlags.StringVar(&upstreamDNSFlag, "upstream-dns", "", "Upstream DNS server(s) (comma-separated, default: 8.8.8.8:53)")
var matchDomainsFlag string
serviceFlags.StringVar(&matchDomainsFlag, "match-domains", "", "FQDN wildcard patterns (comma-separated, e.g. '*.proxy.internal,*.host-0?.autoco.internal') to check against local records/upstream DNS; queries for non-matching domains are sent directly to the system's DNS servers (default: match all domains)")
serviceFlags.StringVar(&config.LogLevel, "log-level", config.LogLevel, "Log level (DEBUG, INFO, WARN, ERROR, FATAL)")
serviceFlags.StringVar(&config.InterfaceName, "interface", config.InterfaceName, "Name of the WireGuard interface")
serviceFlags.StringVar(&config.HTTPAddr, "http-addr", config.HTTPAddr, "HTTP server address (e.g., ':9452')")
@@ -348,6 +363,11 @@ func loadConfigFromCLI(config *OlmConfig, args []string) (bool, bool, error) {
}
}
// Parse match domains flag if provided
if matchDomainsFlag != "" {
config.MatchDomains = splitComma(matchDomainsFlag)
}
// Track which values were changed by CLI args
if config.Endpoint != origValues["endpoint"].(string) {
config.sources["endpoint"] = string(SourceCLI)
@@ -373,6 +393,9 @@ func loadConfigFromCLI(config *OlmConfig, args []string) (bool, bool, error) {
if fmt.Sprintf("%v", config.UpstreamDNS) != origValues["upstreamDNS"].(string) {
config.sources["upstreamDNS"] = string(SourceCLI)
}
if fmt.Sprintf("%v", config.MatchDomains) != origValues["matchDomains"].(string) {
config.sources["matchDomains"] = string(SourceCLI)
}
if config.LogLevel != origValues["logLevel"].(string) {
config.sources["logLevel"] = string(SourceCLI)
}
@@ -481,6 +504,10 @@ func mergeConfigs(dest, src *OlmConfig) {
dest.UpstreamDNS = src.UpstreamDNS
dest.sources["upstreamDNS"] = string(SourceFile)
}
if len(src.MatchDomains) > 0 {
dest.MatchDomains = src.MatchDomains
dest.sources["matchDomains"] = string(SourceFile)
}
if src.LogLevel != "" && src.LogLevel != "INFO" {
dest.LogLevel = src.LogLevel
dest.sources["logLevel"] = string(SourceFile)
@@ -598,6 +625,7 @@ func (c *OlmConfig) ShowConfig() {
fmt.Printf(" mtu = %d [%s]\n", c.MTU, getSource("mtu"))
fmt.Printf(" dns = %s [%s]\n", c.DNS, getSource("dns"))
fmt.Printf(" upstream-dns = %v [%s]\n", c.UpstreamDNS, getSource("upstreamDNS"))
fmt.Printf(" match-domains = %v [%s]\n", c.MatchDomains, getSource("matchDomains"))
fmt.Printf(" interface = %s [%s]\n", c.InterfaceName, getSource("interface"))
// Logging

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"net"
"net/netip"
"strings"
"sync"
"time"
@@ -38,6 +39,20 @@ type DNSProxy struct {
middleDevice *device.MiddleDevice // Reference to MiddleDevice for packet filtering and TUN writes
recordStore *DNSRecordStore // Local DNS records
// matchDomains lists the FQDN wildcard patterns (using * and ? wildcards, see
// matchWildcard) that this proxy is responsible for. Queries whose name matches
// one of these patterns are checked against local records and, failing that,
// forwarded to upstreamDNS. Queries that match none of the patterns are sent
// directly to localDNS instead, bypassing local records and upstreamDNS
// entirely. An empty matchDomains means "match everything" (i.e. behave as if
// this feature were not configured).
matchDomains []string
// localDNS holds the host's own system DNS servers (as reported by
// SystemDNSMonitor / PublicDNS), used to resolve queries that don't match
// matchDomains rather than sending them upstream or through the tunnel.
localDNS []string
matchMu sync.RWMutex
// Tunnel DNS fields - for sending queries over WireGuard
tunnelIP netip.Addr // WireGuard interface IP (source for tunneled queries)
tunnelStack *stack.Stack // Separate netstack for outbound tunnel queries
@@ -55,8 +70,14 @@ type DNSProxy struct {
wg sync.WaitGroup
}
// NewDNSProxy creates a new DNS proxy
func NewDNSProxy(middleDevice *device.MiddleDevice, mtu int, utilitySubnet string, upstreamDns []string, tunnelDns bool, tunnelIP string) (*DNSProxy, error) {
// NewDNSProxy creates a new DNS proxy.
//
// matchDomains, if non-empty, restricts local-record lookup and upstream
// forwarding to queries whose name matches one of the given wildcard patterns
// (see matchWildcard). Queries that match none of the patterns are instead
// forwarded directly to localDNS (the host's own system DNS servers). Pass an
// empty matchDomains to match every query, preserving prior behavior.
func NewDNSProxy(middleDevice *device.MiddleDevice, mtu int, utilitySubnet string, upstreamDns []string, tunnelDns bool, tunnelIP string, matchDomains []string, localDNS []string) (*DNSProxy, error) {
proxyIP, err := PickIPFromSubnet(utilitySubnet)
if err != nil {
return nil, fmt.Errorf("failed to pick DNS proxy IP from subnet: %v", err)
@@ -76,6 +97,8 @@ func NewDNSProxy(middleDevice *device.MiddleDevice, mtu int, utilitySubnet strin
tunnelDNS: tunnelDns,
recordStore: NewDNSRecordStore(),
tunnelActivePorts: make(map[uint16]bool),
matchDomains: matchDomains,
localDNS: localDNS,
ctx: ctx,
cancel: cancel,
}
@@ -383,6 +406,27 @@ func (p *DNSProxy) handleDNSQuery(udpConn *gonet.UDPConn, queryData []byte, clie
question := msg.Question[0]
logger.Debug("DNS query for %s (type %s)", question.Name, dns.TypeToString[question.Qtype])
// If matchDomains is configured and this query's name doesn't match any of
// the configured patterns, skip local records and upstream entirely and
// send it straight to the host's own system DNS servers.
if !p.matchesConfiguredDomains(question.Name) {
logger.Debug("Query for %s does not match configured domains, forwarding to local DNS %v", question.Name, p.getLocalDNS())
response := p.forwardToLocalDNS(msg)
if response == nil {
logger.Error("Failed to get DNS response for %s from local DNS", question.Name)
return
}
responseData, err := response.Pack()
if err != nil {
logger.Error("Failed to pack DNS response: %v", err)
return
}
if _, err := udpConn.WriteTo(responseData, clientAddr); err != nil {
logger.Error("Failed to send DNS response: %v", err)
}
return
}
// Check if we have local records for this query
var response *dns.Msg
if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA || question.Qtype == dns.TypePTR {
@@ -505,6 +549,77 @@ func (p *DNSProxy) checkLocalRecords(query *dns.Msg, question dns.Question) *dns
return response
}
// matchesConfiguredDomains reports whether name matches one of the configured
// matchDomains wildcard patterns. If matchDomains is empty, every name is
// considered a match (i.e. the feature is disabled).
func (p *DNSProxy) matchesConfiguredDomains(name string) bool {
p.matchMu.RLock()
patterns := p.matchDomains
p.matchMu.RUnlock()
if len(patterns) == 0 {
return true
}
name = strings.ToLower(dns.Fqdn(name))
for _, pattern := range patterns {
pattern = strings.ToLower(dns.Fqdn(pattern))
if matchWildcard(pattern, name) {
return true
}
}
return false
}
// getLocalDNS returns the currently configured local (system) DNS servers.
func (p *DNSProxy) getLocalDNS() []string {
p.matchMu.RLock()
defer p.matchMu.RUnlock()
return p.localDNS
}
// forwardToLocalDNS forwards a DNS query directly to the host's own system DNS
// servers (localDNS), always using host networking regardless of tunnelDNS -
// these queries are for domains the caller has explicitly excluded from
// Pangolin resolution, so they should never traverse the tunnel.
func (p *DNSProxy) forwardToLocalDNS(query *dns.Msg) *dns.Msg {
servers := p.getLocalDNS()
if len(servers) == 0 {
logger.Warn("No local DNS servers configured, dropping query for %s", query.Question[0].Name)
return nil
}
var lastErr error
for _, server := range servers {
response, err := p.queryUpstreamDirect(server, query, 2*time.Second)
if err == nil {
return response
}
lastErr = err
}
logger.Error("All local DNS servers failed: %v", lastErr)
return nil
}
// SetMatchDomains replaces the list of wildcard domain patterns (see
// matchWildcard) that this proxy checks against local records / upstream DNS.
// Queries not matching any pattern are sent to localDNS instead. Pass an
// empty slice to match every query (i.e. disable filtering).
func (p *DNSProxy) SetMatchDomains(patterns []string) {
p.matchMu.Lock()
defer p.matchMu.Unlock()
p.matchDomains = patterns
}
// SetLocalDNS replaces the list of local (host system) DNS servers used to
// resolve queries that don't match matchDomains. Servers must be in
// "host:port" format (e.g. "192.168.1.1:53").
func (p *DNSProxy) SetLocalDNS(servers []string) {
p.matchMu.Lock()
defer p.matchMu.Unlock()
p.localDNS = servers
}
// forwardToUpstream forwards a DNS query to upstream DNS servers
func (p *DNSProxy) forwardToUpstream(query *dns.Msg) *dns.Msg {
// Try primary DNS server

View File

@@ -263,6 +263,7 @@ func runOlmMainWithArgs(ctx context.Context, cancel context.CancelFunc, signalCt
MTU: config.MTU,
DNS: config.DNS,
UpstreamDNS: config.UpstreamDNS,
MatchDomains: config.MatchDomains,
InterfaceName: config.InterfaceName,
Holepunch: !config.DisableHolepunch,
TlsClientCert: config.TlsClientCert,

View File

@@ -145,7 +145,7 @@ func (o *Olm) handleConnect(msg websocket.WSMessage) {
}
// Create and start DNS proxy
o.dnsProxy, err = dns.NewDNSProxy(o.middleDev, o.tunnelConfig.MTU, wgData.UtilitySubnet, o.tunnelConfig.UpstreamDNS, o.tunnelConfig.TunnelDNS, interfaceIP)
o.dnsProxy, err = dns.NewDNSProxy(o.middleDev, o.tunnelConfig.MTU, wgData.UtilitySubnet, o.tunnelConfig.UpstreamDNS, o.tunnelConfig.TunnelDNS, interfaceIP, o.tunnelConfig.MatchDomains, o.tunnelConfig.PublicDNS)
if err != nil {
logger.Error("Failed to create DNS proxy: %v", err)
}

View File

@@ -231,6 +231,7 @@ func (o *Olm) registerAPICallbacks() {
Holepunch: req.Holepunch,
TlsClientCert: req.TlsClientCert,
OrgID: req.OrgID,
MatchDomains: req.MatchDomains,
}
var err error
@@ -427,6 +428,11 @@ func (o *Olm) StartTunnel(config TunnelConfig) {
if pm := o.getPeerManager(); pm != nil {
pm.SetPublicDNS(servers)
}
// Keep the DNS proxy's local-DNS fallback (used for MatchDomains
// misses) in sync with the host's real system DNS servers.
if o.dnsProxy != nil {
o.dnsProxy.SetLocalDNS(servers)
}
// UpstreamDNS is updated only when the caller did not supply an
// explicit value; dynamic updates keep the proxy forwarding to the

View File

@@ -79,6 +79,13 @@ type TunnelConfig struct {
PublicDNS []string
InterfaceName string
// MatchDomains lists FQDN wildcard patterns (using * and ? wildcards) that
// olm should check against local records / resolve via UpstreamDNS. Queries
// that don't match any pattern are sent directly to the host's own system
// DNS servers (PublicDNS) instead of being handled by the DNS proxy at all.
// An empty MatchDomains matches every query, preserving prior behavior.
MatchDomains []string
// Advanced
Holepunch bool
TlsClientCert string