mirror of
https://github.com/DNSCrypt/dnscrypt-proxy.git
synced 2025-04-05 14:17:36 +03:00
Fix system dns is ignored if bootstrap resolver is empty. Fix default bootstrap resolver is not used if bootstrap resolver is empty.
1000 lines
35 KiB
Go
1000 lines
35 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"math/rand"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
"github.com/jedisct1/dlog"
|
|
stamps "github.com/jedisct1/go-dnsstamps"
|
|
netproxy "golang.org/x/net/proxy"
|
|
)
|
|
|
|
const (
|
|
MaxTimeout = 3600
|
|
DefaultNetprobeAddress = "9.9.9.9:53"
|
|
)
|
|
|
|
type Config struct {
|
|
LogLevel int `toml:"log_level"`
|
|
LogFile *string `toml:"log_file"`
|
|
LogFileLatest bool `toml:"log_file_latest"`
|
|
UseSyslog bool `toml:"use_syslog"`
|
|
ServerNames []string `toml:"server_names"`
|
|
DisabledServerNames []string `toml:"disabled_server_names"`
|
|
ListenAddresses []string `toml:"listen_addresses"`
|
|
LocalDoH LocalDoHConfig `toml:"local_doh"`
|
|
UserName string `toml:"user_name"`
|
|
ForceTCP bool `toml:"force_tcp"`
|
|
HTTP3 bool `toml:"http3"`
|
|
Timeout int `toml:"timeout"`
|
|
KeepAlive int `toml:"keepalive"`
|
|
Proxy string `toml:"proxy"`
|
|
CertRefreshConcurrency int `toml:"cert_refresh_concurrency"`
|
|
CertRefreshDelay int `toml:"cert_refresh_delay"`
|
|
CertIgnoreTimestamp bool `toml:"cert_ignore_timestamp"`
|
|
EphemeralKeys bool `toml:"dnscrypt_ephemeral_keys"`
|
|
LBStrategy string `toml:"lb_strategy"`
|
|
LBEstimator bool `toml:"lb_estimator"`
|
|
BlockIPv6 bool `toml:"block_ipv6"`
|
|
BlockUnqualified bool `toml:"block_unqualified"`
|
|
BlockUndelegated bool `toml:"block_undelegated"`
|
|
Cache bool
|
|
CacheSize int `toml:"cache_size"`
|
|
CacheNegTTL uint32 `toml:"cache_neg_ttl"`
|
|
CacheNegMinTTL uint32 `toml:"cache_neg_min_ttl"`
|
|
CacheNegMaxTTL uint32 `toml:"cache_neg_max_ttl"`
|
|
CacheMinTTL uint32 `toml:"cache_min_ttl"`
|
|
CacheMaxTTL uint32 `toml:"cache_max_ttl"`
|
|
RejectTTL uint32 `toml:"reject_ttl"`
|
|
CloakTTL uint32 `toml:"cloak_ttl"`
|
|
QueryLog QueryLogConfig `toml:"query_log"`
|
|
NxLog NxLogConfig `toml:"nx_log"`
|
|
BlockName BlockNameConfig `toml:"blocked_names"`
|
|
BlockNameLegacy BlockNameConfigLegacy `toml:"blacklist"`
|
|
WhitelistNameLegacy WhitelistNameConfigLegacy `toml:"whitelist"`
|
|
AllowedName AllowedNameConfig `toml:"allowed_names"`
|
|
BlockIP BlockIPConfig `toml:"blocked_ips"`
|
|
BlockIPLegacy BlockIPConfigLegacy `toml:"ip_blacklist"`
|
|
AllowIP AllowIPConfig `toml:"allowed_ips"`
|
|
ForwardFile string `toml:"forwarding_rules"`
|
|
CloakFile string `toml:"cloaking_rules"`
|
|
CaptivePortals CaptivePortalsConfig `toml:"captive_portals"`
|
|
StaticsConfig map[string]StaticConfig `toml:"static"`
|
|
SourcesConfig map[string]SourceConfig `toml:"sources"`
|
|
BrokenImplementations BrokenImplementationsConfig `toml:"broken_implementations"`
|
|
SourceRequireDNSSEC bool `toml:"require_dnssec"`
|
|
SourceRequireNoLog bool `toml:"require_nolog"`
|
|
SourceRequireNoFilter bool `toml:"require_nofilter"`
|
|
SourceDNSCrypt bool `toml:"dnscrypt_servers"`
|
|
SourceDoH bool `toml:"doh_servers"`
|
|
SourceODoH bool `toml:"odoh_servers"`
|
|
SourceIPv4 bool `toml:"ipv4_servers"`
|
|
SourceIPv6 bool `toml:"ipv6_servers"`
|
|
MaxClients uint32 `toml:"max_clients"`
|
|
BootstrapResolversLegacy []string `toml:"fallback_resolvers"`
|
|
BootstrapResolvers []string `toml:"bootstrap_resolvers"`
|
|
IgnoreSystemDNS bool `toml:"ignore_system_dns"`
|
|
AllWeeklyRanges map[string]WeeklyRangesStr `toml:"schedules"`
|
|
LogMaxSize int `toml:"log_files_max_size"`
|
|
LogMaxAge int `toml:"log_files_max_age"`
|
|
LogMaxBackups int `toml:"log_files_max_backups"`
|
|
TLSDisableSessionTickets bool `toml:"tls_disable_session_tickets"`
|
|
TLSCipherSuite []uint16 `toml:"tls_cipher_suite"`
|
|
TLSKeyLogFile string `toml:"tls_key_log_file"`
|
|
NetprobeAddress string `toml:"netprobe_address"`
|
|
NetprobeTimeout int `toml:"netprobe_timeout"`
|
|
OfflineMode bool `toml:"offline_mode"`
|
|
HTTPProxyURL string `toml:"http_proxy"`
|
|
RefusedCodeInResponses bool `toml:"refused_code_in_responses"`
|
|
BlockedQueryResponse string `toml:"blocked_query_response"`
|
|
QueryMeta []string `toml:"query_meta"`
|
|
CloakedPTR bool `toml:"cloak_ptr"`
|
|
AnonymizedDNS AnonymizedDNSConfig `toml:"anonymized_dns"`
|
|
DoHClientX509Auth DoHClientX509AuthConfig `toml:"doh_client_x509_auth"`
|
|
DoHClientX509AuthLegacy DoHClientX509AuthConfig `toml:"tls_client_auth"`
|
|
DNS64 DNS64Config `toml:"dns64"`
|
|
EDNSClientSubnet []string `toml:"edns_client_subnet"`
|
|
}
|
|
|
|
func newConfig() Config {
|
|
return Config{
|
|
LogLevel: int(dlog.LogLevel()),
|
|
LogFileLatest: true,
|
|
ListenAddresses: []string{"127.0.0.1:53"},
|
|
LocalDoH: LocalDoHConfig{Path: "/dns-query"},
|
|
Timeout: 5000,
|
|
KeepAlive: 5,
|
|
CertRefreshConcurrency: 10,
|
|
CertRefreshDelay: 240,
|
|
HTTP3: false,
|
|
CertIgnoreTimestamp: false,
|
|
EphemeralKeys: false,
|
|
Cache: true,
|
|
CacheSize: 512,
|
|
CacheNegTTL: 0,
|
|
CacheNegMinTTL: 60,
|
|
CacheNegMaxTTL: 600,
|
|
CacheMinTTL: 60,
|
|
CacheMaxTTL: 86400,
|
|
RejectTTL: 600,
|
|
CloakTTL: 600,
|
|
SourceRequireNoLog: true,
|
|
SourceRequireNoFilter: true,
|
|
SourceIPv4: true,
|
|
SourceIPv6: false,
|
|
SourceDNSCrypt: true,
|
|
SourceDoH: true,
|
|
SourceODoH: false,
|
|
MaxClients: 250,
|
|
BootstrapResolvers: []string{DefaultBootstrapResolver},
|
|
IgnoreSystemDNS: false,
|
|
LogMaxSize: 10,
|
|
LogMaxAge: 7,
|
|
LogMaxBackups: 1,
|
|
TLSDisableSessionTickets: false,
|
|
TLSCipherSuite: nil,
|
|
TLSKeyLogFile: "",
|
|
NetprobeTimeout: 60,
|
|
OfflineMode: false,
|
|
RefusedCodeInResponses: false,
|
|
LBEstimator: true,
|
|
BlockedQueryResponse: "hinfo",
|
|
BrokenImplementations: BrokenImplementationsConfig{
|
|
FragmentsBlocked: []string{
|
|
"cisco", "cisco-ipv6", "cisco-familyshield", "cisco-familyshield-ipv6",
|
|
"cleanbrowsing-adult", "cleanbrowsing-adult-ipv6", "cleanbrowsing-family", "cleanbrowsing-family-ipv6", "cleanbrowsing-security", "cleanbrowsing-security-ipv6",
|
|
},
|
|
},
|
|
AnonymizedDNS: AnonymizedDNSConfig{
|
|
DirectCertFallback: true,
|
|
},
|
|
CloakedPTR: false,
|
|
}
|
|
}
|
|
|
|
type StaticConfig struct {
|
|
Stamp string
|
|
}
|
|
|
|
type SourceConfig struct {
|
|
URL string
|
|
URLs []string
|
|
MinisignKeyStr string `toml:"minisign_key"`
|
|
CacheFile string `toml:"cache_file"`
|
|
FormatStr string `toml:"format"`
|
|
RefreshDelay int `toml:"refresh_delay"`
|
|
Prefix string
|
|
}
|
|
|
|
type QueryLogConfig struct {
|
|
File string
|
|
Format string
|
|
IgnoredQtypes []string `toml:"ignored_qtypes"`
|
|
}
|
|
|
|
type NxLogConfig struct {
|
|
File string
|
|
Format string
|
|
}
|
|
|
|
type BlockNameConfig struct {
|
|
File string `toml:"blocked_names_file"`
|
|
LogFile string `toml:"log_file"`
|
|
Format string `toml:"log_format"`
|
|
}
|
|
|
|
type BlockNameConfigLegacy struct {
|
|
File string `toml:"blacklist_file"`
|
|
LogFile string `toml:"log_file"`
|
|
Format string `toml:"log_format"`
|
|
}
|
|
|
|
type WhitelistNameConfigLegacy struct {
|
|
File string `toml:"whitelist_file"`
|
|
LogFile string `toml:"log_file"`
|
|
Format string `toml:"log_format"`
|
|
}
|
|
|
|
type AllowedNameConfig struct {
|
|
File string `toml:"allowed_names_file"`
|
|
LogFile string `toml:"log_file"`
|
|
Format string `toml:"log_format"`
|
|
}
|
|
|
|
type BlockIPConfig struct {
|
|
File string `toml:"blocked_ips_file"`
|
|
LogFile string `toml:"log_file"`
|
|
Format string `toml:"log_format"`
|
|
}
|
|
|
|
type BlockIPConfigLegacy struct {
|
|
File string `toml:"blacklist_file"`
|
|
LogFile string `toml:"log_file"`
|
|
Format string `toml:"log_format"`
|
|
}
|
|
|
|
type AllowIPConfig struct {
|
|
File string `toml:"allowed_ips_file"`
|
|
LogFile string `toml:"log_file"`
|
|
Format string `toml:"log_format"`
|
|
}
|
|
|
|
type AnonymizedDNSRouteConfig struct {
|
|
ServerName string `toml:"server_name"`
|
|
RelayNames []string `toml:"via"`
|
|
}
|
|
|
|
type AnonymizedDNSConfig struct {
|
|
Routes []AnonymizedDNSRouteConfig `toml:"routes"`
|
|
SkipIncompatible bool `toml:"skip_incompatible"`
|
|
DirectCertFallback bool `toml:"direct_cert_fallback"`
|
|
}
|
|
|
|
type BrokenImplementationsConfig struct {
|
|
BrokenQueryPadding []string `toml:"broken_query_padding"`
|
|
FragmentsBlocked []string `toml:"fragments_blocked"`
|
|
}
|
|
|
|
type LocalDoHConfig struct {
|
|
ListenAddresses []string `toml:"listen_addresses"`
|
|
Path string `toml:"path"`
|
|
CertFile string `toml:"cert_file"`
|
|
CertKeyFile string `toml:"cert_key_file"`
|
|
}
|
|
|
|
type ServerSummary struct {
|
|
Name string `json:"name"`
|
|
Proto string `json:"proto"`
|
|
IPv6 bool `json:"ipv6"`
|
|
Addrs []string `json:"addrs,omitempty"`
|
|
Ports []int `json:"ports"`
|
|
DNSSEC *bool `json:"dnssec,omitempty"`
|
|
NoLog bool `json:"nolog"`
|
|
NoFilter bool `json:"nofilter"`
|
|
Description string `json:"description,omitempty"`
|
|
Stamp string `json:"stamp"`
|
|
}
|
|
|
|
type TLSClientAuthCredsConfig struct {
|
|
ServerName string `toml:"server_name"`
|
|
ClientCert string `toml:"client_cert"`
|
|
ClientKey string `toml:"client_key"`
|
|
RootCA string `toml:"root_ca"`
|
|
}
|
|
|
|
type DoHClientX509AuthConfig struct {
|
|
Creds []TLSClientAuthCredsConfig `toml:"creds"`
|
|
}
|
|
|
|
type DNS64Config struct {
|
|
Prefixes []string `toml:"prefix"`
|
|
Resolvers []string `toml:"resolver"`
|
|
}
|
|
|
|
type CaptivePortalsConfig struct {
|
|
MapFile string `toml:"map_file"`
|
|
}
|
|
|
|
type ConfigFlags struct {
|
|
Resolve *string
|
|
List *bool
|
|
ListAll *bool
|
|
IncludeRelays *bool
|
|
JSONOutput *bool
|
|
Check *bool
|
|
ConfigFile *string
|
|
Child *bool
|
|
NetprobeTimeoutOverride *int
|
|
ShowCerts *bool
|
|
}
|
|
|
|
func findConfigFile(configFile *string) (string, error) {
|
|
if _, err := os.Stat(*configFile); os.IsNotExist(err) {
|
|
cdLocal()
|
|
if _, err := os.Stat(*configFile); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
pwd, err := os.Getwd()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if filepath.IsAbs(*configFile) {
|
|
return *configFile, nil
|
|
}
|
|
return path.Join(pwd, *configFile), nil
|
|
}
|
|
|
|
func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error {
|
|
foundConfigFile, err := findConfigFile(flags.ConfigFile)
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"Unable to load the configuration file [%s] -- Maybe use the -config command-line switch?",
|
|
*flags.ConfigFile,
|
|
)
|
|
}
|
|
WarnIfMaybeWritableByOtherUsers(foundConfigFile)
|
|
config := newConfig()
|
|
md, err := toml.DecodeFile(foundConfigFile, &config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if flags.Resolve != nil && len(*flags.Resolve) > 0 {
|
|
addr := "127.0.0.1:53"
|
|
if len(config.ListenAddresses) > 0 {
|
|
addr = config.ListenAddresses[0]
|
|
}
|
|
Resolve(addr, *flags.Resolve, len(config.ServerNames) == 1)
|
|
os.Exit(0)
|
|
}
|
|
|
|
if err := cdFileDir(foundConfigFile); err != nil {
|
|
return err
|
|
}
|
|
if config.LogLevel >= 0 && config.LogLevel < int(dlog.SeverityLast) {
|
|
dlog.SetLogLevel(dlog.Severity(config.LogLevel))
|
|
}
|
|
if dlog.LogLevel() <= dlog.SeverityDebug && os.Getenv("DEBUG") == "" {
|
|
dlog.SetLogLevel(dlog.SeverityInfo)
|
|
}
|
|
dlog.TruncateLogFile(config.LogFileLatest)
|
|
proxy.showCerts = *flags.ShowCerts || len(os.Getenv("SHOW_CERTS")) > 0
|
|
isCommandMode := *flags.Check || proxy.showCerts || *flags.List || *flags.ListAll
|
|
if isCommandMode {
|
|
} else if config.UseSyslog {
|
|
dlog.UseSyslog(true)
|
|
} else if config.LogFile != nil {
|
|
dlog.UseLogFile(*config.LogFile)
|
|
if !*flags.Child {
|
|
FileDescriptors = append(FileDescriptors, dlog.GetFileDescriptor())
|
|
} else {
|
|
dlog.SetFileDescriptor(os.NewFile(uintptr(InheritedDescriptorsBase+FileDescriptorNum), "logFile"))
|
|
FileDescriptorNum++
|
|
}
|
|
}
|
|
if !*flags.Child {
|
|
dlog.Noticef("dnscrypt-proxy %s", AppVersion)
|
|
}
|
|
undecoded := md.Undecoded()
|
|
if len(undecoded) > 0 {
|
|
return fmt.Errorf("Unsupported key in configuration file: [%s]", undecoded[0])
|
|
}
|
|
|
|
proxy.logMaxSize = config.LogMaxSize
|
|
proxy.logMaxAge = config.LogMaxAge
|
|
proxy.logMaxBackups = config.LogMaxBackups
|
|
|
|
proxy.userName = config.UserName
|
|
|
|
proxy.child = *flags.Child
|
|
proxy.xTransport = NewXTransport()
|
|
proxy.xTransport.tlsDisableSessionTickets = config.TLSDisableSessionTickets
|
|
proxy.xTransport.tlsCipherSuite = config.TLSCipherSuite
|
|
proxy.xTransport.mainProto = proxy.mainProto
|
|
proxy.xTransport.http3 = config.HTTP3
|
|
if len(config.BootstrapResolvers) == 0 && len(config.BootstrapResolversLegacy) > 0 {
|
|
dlog.Warnf("fallback_resolvers was renamed to bootstrap_resolvers - Please update your configuration")
|
|
config.BootstrapResolvers = config.BootstrapResolversLegacy
|
|
}
|
|
if len(config.BootstrapResolvers) > 0 {
|
|
for _, resolver := range config.BootstrapResolvers {
|
|
if err := isIPAndPort(resolver); err != nil {
|
|
return fmt.Errorf("Bootstrap resolver [%v]: %v", resolver, err)
|
|
}
|
|
}
|
|
proxy.xTransport.bootstrapResolvers = config.BootstrapResolvers
|
|
}
|
|
proxy.xTransport.ignoreSystemDNS = config.IgnoreSystemDNS
|
|
proxy.xTransport.useIPv4 = config.SourceIPv4
|
|
proxy.xTransport.useIPv6 = config.SourceIPv6
|
|
proxy.xTransport.keepAlive = time.Duration(config.KeepAlive) * time.Second
|
|
if len(config.HTTPProxyURL) > 0 {
|
|
httpProxyURL, err := url.Parse(config.HTTPProxyURL)
|
|
if err != nil {
|
|
return fmt.Errorf("Unable to parse the HTTP proxy URL [%v]", config.HTTPProxyURL)
|
|
}
|
|
proxy.xTransport.httpProxyFunction = http.ProxyURL(httpProxyURL)
|
|
}
|
|
|
|
if len(config.Proxy) > 0 {
|
|
proxyDialerURL, err := url.Parse(config.Proxy)
|
|
if err != nil {
|
|
return fmt.Errorf("Unable to parse the proxy URL [%v]", config.Proxy)
|
|
}
|
|
proxyDialer, err := netproxy.FromURL(proxyDialerURL, netproxy.Direct)
|
|
if err != nil {
|
|
return fmt.Errorf("Unable to use the proxy: [%v]", err)
|
|
}
|
|
proxy.xTransport.proxyDialer = &proxyDialer
|
|
proxy.mainProto = "tcp"
|
|
}
|
|
|
|
proxy.xTransport.rebuildTransport()
|
|
|
|
if md.IsDefined("refused_code_in_responses") {
|
|
dlog.Notice("config option `refused_code_in_responses` is deprecated, use `blocked_query_response`")
|
|
if config.RefusedCodeInResponses {
|
|
config.BlockedQueryResponse = "refused"
|
|
} else {
|
|
config.BlockedQueryResponse = "hinfo"
|
|
}
|
|
}
|
|
proxy.blockedQueryResponse = config.BlockedQueryResponse
|
|
proxy.timeout = time.Duration(config.Timeout) * time.Millisecond
|
|
proxy.maxClients = config.MaxClients
|
|
proxy.mainProto = "udp"
|
|
if config.ForceTCP {
|
|
proxy.mainProto = "tcp"
|
|
}
|
|
proxy.certRefreshConcurrency = Max(1, config.CertRefreshConcurrency)
|
|
proxy.certRefreshDelay = time.Duration(Max(60, config.CertRefreshDelay)) * time.Minute
|
|
proxy.certRefreshDelayAfterFailure = time.Duration(10 * time.Second)
|
|
proxy.certIgnoreTimestamp = config.CertIgnoreTimestamp
|
|
proxy.ephemeralKeys = config.EphemeralKeys
|
|
if len(config.ListenAddresses) == 0 && len(config.LocalDoH.ListenAddresses) == 0 {
|
|
dlog.Debug("No local IP/port configured")
|
|
}
|
|
lbStrategy := LBStrategy(DefaultLBStrategy)
|
|
switch lbStrategyStr := strings.ToLower(config.LBStrategy); lbStrategyStr {
|
|
case "":
|
|
// default
|
|
case "p2":
|
|
lbStrategy = LBStrategyP2{}
|
|
case "ph":
|
|
lbStrategy = LBStrategyPH{}
|
|
case "fastest":
|
|
case "first":
|
|
lbStrategy = LBStrategyFirst{}
|
|
case "random":
|
|
lbStrategy = LBStrategyRandom{}
|
|
default:
|
|
if strings.HasPrefix(lbStrategyStr, "p") {
|
|
n, err := strconv.ParseInt(strings.TrimPrefix(lbStrategyStr, "p"), 10, 32)
|
|
if err != nil || n <= 0 {
|
|
dlog.Warnf("Invalid load balancing strategy: [%s]", config.LBStrategy)
|
|
} else {
|
|
lbStrategy = LBStrategyPN{n: int(n)}
|
|
}
|
|
} else {
|
|
dlog.Warnf("Unknown load balancing strategy: [%s]", config.LBStrategy)
|
|
}
|
|
}
|
|
proxy.serversInfo.lbStrategy = lbStrategy
|
|
proxy.serversInfo.lbEstimator = config.LBEstimator
|
|
|
|
proxy.listenAddresses = config.ListenAddresses
|
|
proxy.localDoHListenAddresses = config.LocalDoH.ListenAddresses
|
|
if len(config.LocalDoH.Path) > 0 && config.LocalDoH.Path[0] != '/' {
|
|
return fmt.Errorf("local DoH: [%s] cannot be a valid URL path. Read the documentation", config.LocalDoH.Path)
|
|
}
|
|
proxy.localDoHPath = config.LocalDoH.Path
|
|
proxy.localDoHCertFile = config.LocalDoH.CertFile
|
|
proxy.localDoHCertKeyFile = config.LocalDoH.CertKeyFile
|
|
proxy.pluginBlockIPv6 = config.BlockIPv6
|
|
proxy.pluginBlockUnqualified = config.BlockUnqualified
|
|
proxy.pluginBlockUndelegated = config.BlockUndelegated
|
|
proxy.cache = config.Cache
|
|
proxy.cacheSize = config.CacheSize
|
|
|
|
if config.CacheNegTTL > 0 {
|
|
proxy.cacheNegMinTTL = config.CacheNegTTL
|
|
proxy.cacheNegMaxTTL = config.CacheNegTTL
|
|
} else {
|
|
proxy.cacheNegMinTTL = config.CacheNegMinTTL
|
|
proxy.cacheNegMaxTTL = config.CacheNegMaxTTL
|
|
}
|
|
|
|
proxy.cacheMinTTL = config.CacheMinTTL
|
|
proxy.cacheMaxTTL = config.CacheMaxTTL
|
|
proxy.rejectTTL = config.RejectTTL
|
|
proxy.cloakTTL = config.CloakTTL
|
|
proxy.cloakedPTR = config.CloakedPTR
|
|
|
|
proxy.queryMeta = config.QueryMeta
|
|
|
|
if len(config.EDNSClientSubnet) != 0 {
|
|
proxy.ednsClientSubnets = make([]*net.IPNet, 0)
|
|
for _, cidr := range config.EDNSClientSubnet {
|
|
_, net, err := net.ParseCIDR(cidr)
|
|
if err != nil {
|
|
return fmt.Errorf("Invalid EDNS-client-subnet CIDR: [%v]", cidr)
|
|
}
|
|
proxy.ednsClientSubnets = append(proxy.ednsClientSubnets, net)
|
|
}
|
|
}
|
|
|
|
if len(config.QueryLog.Format) == 0 {
|
|
config.QueryLog.Format = "tsv"
|
|
} else {
|
|
config.QueryLog.Format = strings.ToLower(config.QueryLog.Format)
|
|
}
|
|
if config.QueryLog.Format != "tsv" && config.QueryLog.Format != "ltsv" {
|
|
return errors.New("Unsupported query log format")
|
|
}
|
|
proxy.queryLogFile = config.QueryLog.File
|
|
proxy.queryLogFormat = config.QueryLog.Format
|
|
proxy.queryLogIgnoredQtypes = config.QueryLog.IgnoredQtypes
|
|
|
|
if len(config.NxLog.Format) == 0 {
|
|
config.NxLog.Format = "tsv"
|
|
} else {
|
|
config.NxLog.Format = strings.ToLower(config.NxLog.Format)
|
|
}
|
|
if config.NxLog.Format != "tsv" && config.NxLog.Format != "ltsv" {
|
|
return errors.New("Unsupported NX log format")
|
|
}
|
|
proxy.nxLogFile = config.NxLog.File
|
|
proxy.nxLogFormat = config.NxLog.Format
|
|
|
|
if len(config.BlockName.File) > 0 && len(config.BlockNameLegacy.File) > 0 {
|
|
return errors.New("Don't specify both [blocked_names] and [blacklist] sections - Update your config file")
|
|
}
|
|
if len(config.BlockNameLegacy.File) > 0 {
|
|
dlog.Notice("Use of [blacklist] is deprecated - Update your config file")
|
|
config.BlockName.File = config.BlockNameLegacy.File
|
|
config.BlockName.Format = config.BlockNameLegacy.Format
|
|
config.BlockName.LogFile = config.BlockNameLegacy.LogFile
|
|
}
|
|
if len(config.BlockName.Format) == 0 {
|
|
config.BlockName.Format = "tsv"
|
|
} else {
|
|
config.BlockName.Format = strings.ToLower(config.BlockName.Format)
|
|
}
|
|
if config.BlockName.Format != "tsv" && config.BlockName.Format != "ltsv" {
|
|
return errors.New("Unsupported block log format")
|
|
}
|
|
proxy.blockNameFile = config.BlockName.File
|
|
proxy.blockNameFormat = config.BlockName.Format
|
|
proxy.blockNameLogFile = config.BlockName.LogFile
|
|
|
|
if len(config.AllowedName.File) > 0 && len(config.WhitelistNameLegacy.File) > 0 {
|
|
return errors.New("Don't specify both [whitelist] and [allowed_names] sections - Update your config file")
|
|
}
|
|
if len(config.WhitelistNameLegacy.File) > 0 {
|
|
dlog.Notice("Use of [whitelist] is deprecated - Update your config file")
|
|
config.AllowedName.File = config.WhitelistNameLegacy.File
|
|
config.AllowedName.Format = config.WhitelistNameLegacy.Format
|
|
config.AllowedName.LogFile = config.WhitelistNameLegacy.LogFile
|
|
}
|
|
if len(config.AllowedName.Format) == 0 {
|
|
config.AllowedName.Format = "tsv"
|
|
} else {
|
|
config.AllowedName.Format = strings.ToLower(config.AllowedName.Format)
|
|
}
|
|
if config.AllowedName.Format != "tsv" && config.AllowedName.Format != "ltsv" {
|
|
return errors.New("Unsupported allowed_names log format")
|
|
}
|
|
proxy.allowNameFile = config.AllowedName.File
|
|
proxy.allowNameFormat = config.AllowedName.Format
|
|
proxy.allowNameLogFile = config.AllowedName.LogFile
|
|
|
|
if len(config.BlockIP.File) > 0 && len(config.BlockIPLegacy.File) > 0 {
|
|
return errors.New("Don't specify both [blocked_ips] and [ip_blacklist] sections - Update your config file")
|
|
}
|
|
if len(config.BlockIPLegacy.File) > 0 {
|
|
dlog.Notice("Use of [ip_blacklist] is deprecated - Update your config file")
|
|
config.BlockIP.File = config.BlockIPLegacy.File
|
|
config.BlockIP.Format = config.BlockIPLegacy.Format
|
|
config.BlockIP.LogFile = config.BlockIPLegacy.LogFile
|
|
}
|
|
if len(config.BlockIP.Format) == 0 {
|
|
config.BlockIP.Format = "tsv"
|
|
} else {
|
|
config.BlockIP.Format = strings.ToLower(config.BlockIP.Format)
|
|
}
|
|
if config.BlockIP.Format != "tsv" && config.BlockIP.Format != "ltsv" {
|
|
return errors.New("Unsupported IP block log format")
|
|
}
|
|
proxy.blockIPFile = config.BlockIP.File
|
|
proxy.blockIPFormat = config.BlockIP.Format
|
|
proxy.blockIPLogFile = config.BlockIP.LogFile
|
|
|
|
if len(config.AllowIP.Format) == 0 {
|
|
config.AllowIP.Format = "tsv"
|
|
} else {
|
|
config.AllowIP.Format = strings.ToLower(config.AllowIP.Format)
|
|
}
|
|
if config.AllowIP.Format != "tsv" && config.AllowIP.Format != "ltsv" {
|
|
return errors.New("Unsupported allowed_ips log format")
|
|
}
|
|
proxy.allowedIPFile = config.AllowIP.File
|
|
proxy.allowedIPFormat = config.AllowIP.Format
|
|
proxy.allowedIPLogFile = config.AllowIP.LogFile
|
|
|
|
proxy.forwardFile = config.ForwardFile
|
|
proxy.cloakFile = config.CloakFile
|
|
proxy.captivePortalMapFile = config.CaptivePortals.MapFile
|
|
|
|
allWeeklyRanges, err := ParseAllWeeklyRanges(config.AllWeeklyRanges)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
proxy.allWeeklyRanges = allWeeklyRanges
|
|
|
|
if configRoutes := config.AnonymizedDNS.Routes; configRoutes != nil {
|
|
routes := make(map[string][]string)
|
|
for _, configRoute := range configRoutes {
|
|
routes[configRoute.ServerName] = configRoute.RelayNames
|
|
}
|
|
proxy.routes = &routes
|
|
}
|
|
proxy.skipAnonIncompatibleResolvers = config.AnonymizedDNS.SkipIncompatible
|
|
proxy.anonDirectCertFallback = config.AnonymizedDNS.DirectCertFallback
|
|
|
|
if len(config.TLSKeyLogFile) > 0 {
|
|
f, err := os.OpenFile(config.TLSKeyLogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
|
|
if err != nil {
|
|
dlog.Fatalf("Unable to create key log file [%s]: [%s]", config.TLSKeyLogFile, err)
|
|
}
|
|
dlog.Warnf("TLS key log file [%s] enabled", config.TLSKeyLogFile)
|
|
proxy.xTransport.keyLogWriter = f
|
|
proxy.xTransport.rebuildTransport()
|
|
}
|
|
|
|
if config.DoHClientX509AuthLegacy.Creds != nil {
|
|
return errors.New("[tls_client_auth] has been renamed to [doh_client_x509_auth] - Update your config file")
|
|
}
|
|
dohClientCreds := config.DoHClientX509Auth.Creds
|
|
if len(dohClientCreds) > 0 {
|
|
dlog.Noticef("Enabling TLS authentication")
|
|
configClientCred := dohClientCreds[0]
|
|
if len(dohClientCreds) > 1 {
|
|
dlog.Fatal("Only one tls_client_auth entry is currently supported")
|
|
}
|
|
proxy.xTransport.tlsClientCreds = DOHClientCreds{
|
|
clientCert: configClientCred.ClientCert,
|
|
clientKey: configClientCred.ClientKey,
|
|
rootCA: configClientCred.RootCA,
|
|
}
|
|
proxy.xTransport.rebuildTransport()
|
|
}
|
|
|
|
// Backwards compatibility
|
|
config.BrokenImplementations.FragmentsBlocked = append(
|
|
config.BrokenImplementations.FragmentsBlocked,
|
|
config.BrokenImplementations.BrokenQueryPadding...)
|
|
|
|
proxy.serversBlockingFragments = config.BrokenImplementations.FragmentsBlocked
|
|
|
|
proxy.dns64Prefixes = config.DNS64.Prefixes
|
|
proxy.dns64Resolvers = config.DNS64.Resolvers
|
|
|
|
if *flags.ListAll {
|
|
config.ServerNames = nil
|
|
config.DisabledServerNames = nil
|
|
config.SourceRequireDNSSEC = false
|
|
config.SourceRequireNoFilter = false
|
|
config.SourceRequireNoLog = false
|
|
config.SourceIPv4 = true
|
|
config.SourceIPv6 = true
|
|
config.SourceDNSCrypt = true
|
|
config.SourceDoH = true
|
|
config.SourceODoH = true
|
|
}
|
|
|
|
var requiredProps stamps.ServerInformalProperties
|
|
if config.SourceRequireDNSSEC {
|
|
requiredProps |= stamps.ServerInformalPropertyDNSSEC
|
|
}
|
|
if config.SourceRequireNoLog {
|
|
requiredProps |= stamps.ServerInformalPropertyNoLog
|
|
}
|
|
if config.SourceRequireNoFilter {
|
|
requiredProps |= stamps.ServerInformalPropertyNoFilter
|
|
}
|
|
proxy.requiredProps = requiredProps
|
|
proxy.ServerNames = config.ServerNames
|
|
proxy.DisabledServerNames = config.DisabledServerNames
|
|
proxy.SourceIPv4 = config.SourceIPv4
|
|
proxy.SourceIPv6 = config.SourceIPv6
|
|
proxy.SourceDNSCrypt = config.SourceDNSCrypt
|
|
proxy.SourceDoH = config.SourceDoH
|
|
proxy.SourceODoH = config.SourceODoH
|
|
|
|
netprobeTimeout := config.NetprobeTimeout
|
|
flag.Visit(func(flag *flag.Flag) {
|
|
if flag.Name == "netprobe-timeout" && flags.NetprobeTimeoutOverride != nil {
|
|
netprobeTimeout = *flags.NetprobeTimeoutOverride
|
|
}
|
|
})
|
|
netprobeAddress := DefaultNetprobeAddress
|
|
if len(config.NetprobeAddress) > 0 {
|
|
netprobeAddress = config.NetprobeAddress
|
|
} else if len(config.BootstrapResolvers) > 0 {
|
|
netprobeAddress = config.BootstrapResolvers[0]
|
|
}
|
|
if !isCommandMode {
|
|
if err := NetProbe(proxy, netprobeAddress, netprobeTimeout); err != nil {
|
|
return err
|
|
}
|
|
for _, listenAddrStr := range proxy.listenAddresses {
|
|
proxy.addDNSListener(listenAddrStr)
|
|
}
|
|
for _, listenAddrStr := range proxy.localDoHListenAddresses {
|
|
proxy.addLocalDoHListener(listenAddrStr)
|
|
}
|
|
if err := proxy.addSystemDListeners(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// if 'userName' is set and we are the parent process drop privilege and exit
|
|
if len(proxy.userName) > 0 && !proxy.child {
|
|
proxy.dropPrivilege(proxy.userName, FileDescriptors)
|
|
return errors.New(
|
|
"Dropping privileges is not supporting on this operating system. Unset `user_name` in the configuration file",
|
|
)
|
|
}
|
|
if !config.OfflineMode {
|
|
if err := config.loadSources(proxy); err != nil {
|
|
return err
|
|
}
|
|
if len(proxy.registeredServers) == 0 {
|
|
return errors.New("None of the servers listed in the server_names list was found in the configured sources.")
|
|
}
|
|
}
|
|
if *flags.List || *flags.ListAll {
|
|
if err := config.printRegisteredServers(proxy, *flags.JSONOutput, *flags.IncludeRelays); err != nil {
|
|
return err
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
if proxy.routes != nil && len(*proxy.routes) > 0 {
|
|
hasSpecificRoutes := false
|
|
for _, server := range proxy.registeredServers {
|
|
if via, ok := (*proxy.routes)[server.name]; ok {
|
|
if server.stamp.Proto != stamps.StampProtoTypeDNSCrypt &&
|
|
server.stamp.Proto != stamps.StampProtoTypeODoHTarget {
|
|
dlog.Errorf(
|
|
"DNS anonymization is only supported with the DNSCrypt and ODoH protocols - Connections to [%v] cannot be anonymized",
|
|
server.name,
|
|
)
|
|
} else {
|
|
dlog.Noticef("Anonymized DNS: routing [%v] via %v", server.name, via)
|
|
}
|
|
hasSpecificRoutes = true
|
|
}
|
|
}
|
|
if via, ok := (*proxy.routes)["*"]; ok {
|
|
if hasSpecificRoutes {
|
|
dlog.Noticef("Anonymized DNS: routing everything else via %v", via)
|
|
} else {
|
|
dlog.Noticef("Anonymized DNS: routing everything via %v", via)
|
|
}
|
|
}
|
|
}
|
|
if *flags.Check {
|
|
dlog.Notice("Configuration successfully checked")
|
|
os.Exit(0)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (config *Config) printRegisteredServers(proxy *Proxy, jsonOutput bool, includeRelays bool) error {
|
|
var summary []ServerSummary
|
|
if includeRelays {
|
|
for _, registeredRelay := range proxy.registeredRelays {
|
|
addrStr, port := registeredRelay.stamp.ServerAddrStr, stamps.DefaultPort
|
|
var hostAddr string
|
|
hostAddr, port = ExtractHostAndPort(addrStr, port)
|
|
addrs := make([]string, 0)
|
|
if (registeredRelay.stamp.Proto == stamps.StampProtoTypeDoH || registeredRelay.stamp.Proto == stamps.StampProtoTypeODoHTarget) &&
|
|
len(registeredRelay.stamp.ProviderName) > 0 {
|
|
providerName := registeredRelay.stamp.ProviderName
|
|
var host string
|
|
host, port = ExtractHostAndPort(providerName, port)
|
|
addrs = append(addrs, host)
|
|
}
|
|
if len(addrStr) > 0 {
|
|
addrs = append(addrs, hostAddr)
|
|
}
|
|
nolog := true
|
|
nofilter := true
|
|
if registeredRelay.stamp.Proto == stamps.StampProtoTypeODoHRelay {
|
|
nolog = registeredRelay.stamp.Props&stamps.ServerInformalPropertyNoLog != 0
|
|
}
|
|
serverSummary := ServerSummary{
|
|
Name: registeredRelay.name,
|
|
Proto: registeredRelay.stamp.Proto.String(),
|
|
IPv6: strings.HasPrefix(addrStr, "["),
|
|
Ports: []int{port},
|
|
Addrs: addrs,
|
|
NoLog: nolog,
|
|
NoFilter: nofilter,
|
|
Description: registeredRelay.description,
|
|
Stamp: registeredRelay.stamp.String(),
|
|
}
|
|
if jsonOutput {
|
|
summary = append(summary, serverSummary)
|
|
} else {
|
|
fmt.Println(serverSummary.Name)
|
|
}
|
|
}
|
|
}
|
|
for _, registeredServer := range proxy.registeredServers {
|
|
addrStr, port := registeredServer.stamp.ServerAddrStr, stamps.DefaultPort
|
|
var hostAddr string
|
|
hostAddr, port = ExtractHostAndPort(addrStr, port)
|
|
addrs := make([]string, 0)
|
|
if (registeredServer.stamp.Proto == stamps.StampProtoTypeDoH || registeredServer.stamp.Proto == stamps.StampProtoTypeODoHTarget) &&
|
|
len(registeredServer.stamp.ProviderName) > 0 {
|
|
providerName := registeredServer.stamp.ProviderName
|
|
var host string
|
|
host, port = ExtractHostAndPort(providerName, port)
|
|
addrs = append(addrs, host)
|
|
}
|
|
if len(addrStr) > 0 {
|
|
addrs = append(addrs, hostAddr)
|
|
}
|
|
dnssec := registeredServer.stamp.Props&stamps.ServerInformalPropertyDNSSEC != 0
|
|
serverSummary := ServerSummary{
|
|
Name: registeredServer.name,
|
|
Proto: registeredServer.stamp.Proto.String(),
|
|
IPv6: strings.HasPrefix(addrStr, "["),
|
|
Ports: []int{port},
|
|
Addrs: addrs,
|
|
DNSSEC: &dnssec,
|
|
NoLog: registeredServer.stamp.Props&stamps.ServerInformalPropertyNoLog != 0,
|
|
NoFilter: registeredServer.stamp.Props&stamps.ServerInformalPropertyNoFilter != 0,
|
|
Description: registeredServer.description,
|
|
Stamp: registeredServer.stamp.String(),
|
|
}
|
|
if jsonOutput {
|
|
summary = append(summary, serverSummary)
|
|
} else {
|
|
fmt.Println(serverSummary.Name)
|
|
}
|
|
}
|
|
if jsonOutput {
|
|
jsonStr, err := json.MarshalIndent(summary, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Print(string(jsonStr))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (config *Config) loadSources(proxy *Proxy) error {
|
|
for cfgSourceName, cfgSource_ := range config.SourcesConfig {
|
|
cfgSource := cfgSource_
|
|
rand.Shuffle(len(cfgSource.URLs), func(i, j int) {
|
|
cfgSource.URLs[i], cfgSource.URLs[j] = cfgSource.URLs[j], cfgSource.URLs[i]
|
|
})
|
|
if err := config.loadSource(proxy, cfgSourceName, &cfgSource); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for name, config := range config.StaticsConfig {
|
|
if stamp, err := stamps.NewServerStampFromString(config.Stamp); err == nil {
|
|
if stamp.Proto == stamps.StampProtoTypeDNSCryptRelay || stamp.Proto == stamps.StampProtoTypeODoHRelay {
|
|
dlog.Debugf("Adding [%s] to the set of available static relays", name)
|
|
registeredServer := RegisteredServer{name: name, stamp: stamp, description: "static relay"}
|
|
proxy.registeredRelays = append(proxy.registeredRelays, registeredServer)
|
|
}
|
|
}
|
|
}
|
|
if len(config.ServerNames) == 0 {
|
|
for serverName := range config.StaticsConfig {
|
|
config.ServerNames = append(config.ServerNames, serverName)
|
|
}
|
|
}
|
|
for _, serverName := range config.ServerNames {
|
|
staticConfig, ok := config.StaticsConfig[serverName]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if len(staticConfig.Stamp) == 0 {
|
|
return fmt.Errorf("Missing stamp for the static [%s] definition", serverName)
|
|
}
|
|
stamp, err := stamps.NewServerStampFromString(staticConfig.Stamp)
|
|
if err != nil {
|
|
return fmt.Errorf("Stamp error for the static [%s] definition: [%v]", serverName, err)
|
|
}
|
|
proxy.registeredServers = append(proxy.registeredServers, RegisteredServer{name: serverName, stamp: stamp})
|
|
}
|
|
if err := proxy.updateRegisteredServers(); err != nil {
|
|
return err
|
|
}
|
|
rs1 := proxy.registeredServers
|
|
rs2 := proxy.serversInfo.registeredServers
|
|
rand.Shuffle(len(rs1), func(i, j int) {
|
|
rs1[i], rs1[j] = rs1[j], rs1[i]
|
|
})
|
|
rand.Shuffle(len(rs2), func(i, j int) {
|
|
rs2[i], rs2[j] = rs2[j], rs2[i]
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (config *Config) loadSource(proxy *Proxy, cfgSourceName string, cfgSource *SourceConfig) error {
|
|
if len(cfgSource.URLs) == 0 {
|
|
if len(cfgSource.URL) == 0 {
|
|
dlog.Debugf("Missing URLs for source [%s]", cfgSourceName)
|
|
} else {
|
|
cfgSource.URLs = []string{cfgSource.URL}
|
|
}
|
|
}
|
|
if cfgSource.MinisignKeyStr == "" {
|
|
return fmt.Errorf("Missing Minisign key for source [%s]", cfgSourceName)
|
|
}
|
|
if cfgSource.CacheFile == "" {
|
|
return fmt.Errorf("Missing cache file for source [%s]", cfgSourceName)
|
|
}
|
|
if cfgSource.FormatStr == "" {
|
|
cfgSource.FormatStr = "v2"
|
|
}
|
|
if cfgSource.RefreshDelay <= 0 {
|
|
cfgSource.RefreshDelay = 72
|
|
}
|
|
cfgSource.RefreshDelay = Min(169, Max(25, cfgSource.RefreshDelay))
|
|
source, err := NewSource(
|
|
cfgSourceName,
|
|
proxy.xTransport,
|
|
cfgSource.URLs,
|
|
cfgSource.MinisignKeyStr,
|
|
cfgSource.CacheFile,
|
|
cfgSource.FormatStr,
|
|
time.Duration(cfgSource.RefreshDelay)*time.Hour,
|
|
cfgSource.Prefix,
|
|
)
|
|
if err != nil {
|
|
if len(source.bin) <= 0 {
|
|
dlog.Criticalf("Unable to retrieve source [%s]: [%s]", cfgSourceName, err)
|
|
return err
|
|
}
|
|
dlog.Infof("Downloading [%s] failed: %v, using cache file to startup", source.name, err)
|
|
}
|
|
proxy.sources = append(proxy.sources, source)
|
|
return nil
|
|
}
|
|
|
|
func includesName(names []string, name string) bool {
|
|
for _, found := range names {
|
|
if strings.EqualFold(found, name) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func cdFileDir(fileName string) error {
|
|
return os.Chdir(filepath.Dir(fileName))
|
|
}
|
|
|
|
func cdLocal() {
|
|
exeFileName, err := os.Executable()
|
|
if err != nil {
|
|
dlog.Warnf(
|
|
"Unable to determine the executable directory: [%s] -- You will need to specify absolute paths in the configuration file",
|
|
err,
|
|
)
|
|
} else if err := os.Chdir(filepath.Dir(exeFileName)); err != nil {
|
|
dlog.Warnf("Unable to change working directory to [%s]: %s", exeFileName, err)
|
|
}
|
|
}
|
|
|
|
func isIPAndPort(addrStr string) error {
|
|
host, port := ExtractHostAndPort(addrStr, -1)
|
|
if ip := ParseIP(host); ip == nil {
|
|
return fmt.Errorf("Host does not parse as IP '%s'", addrStr)
|
|
} else if port == -1 {
|
|
return fmt.Errorf("Port missing '%s'", addrStr)
|
|
} else if _, err := strconv.ParseUint(strconv.Itoa(port), 10, 16); err != nil {
|
|
return fmt.Errorf("Port does not parse '%s' [%v]", addrStr, err)
|
|
}
|
|
return nil
|
|
}
|