From 011f37a4992822fe2e5ac8f11b3d585321696f2c Mon Sep 17 00:00:00 2001 From: Dimitry Kolyshev Date: Thu, 12 Sep 2024 07:06:01 +0300 Subject: [PATCH] Pull request: 182-cmd-args Squashed commit of the following: commit 82901d70b05323d94fb349e6a45d41a26ff7ce57 Author: Dimitry Kolyshev Date: Wed Sep 11 17:16:25 2024 +0700 cmd: imp code commit 9d330d43d886b6cdfb446f2ff107c1b58e9d63f2 Author: Dimitry Kolyshev Date: Wed Sep 11 10:22:59 2024 +0700 cmd: imp code commit ef58ba64f114a8ba6c3b9c474408f7d731f4c1c0 Author: Dimitry Kolyshev Date: Tue Sep 10 10:40:41 2024 +0700 cmd: imp code commit 2c6f53398c6d5918369e13d6e6dd6c74edc8e3c6 Author: Dimitry Kolyshev Date: Mon Sep 9 13:29:59 2024 +0700 cmd: imp code commit d4bc2351939ef5f3131619d60a209206f908c0b3 Author: Dimitry Kolyshev Date: Mon Sep 9 13:08:01 2024 +0700 cmd: imp code commit ccb5798804e150425afbbf7a4a4f37cf084942cc Author: Dimitry Kolyshev Date: Mon Sep 9 12:23:42 2024 +0700 cmd: imp docs commit c50fa6e91000c90490cd190442abc89a241dfad3 Author: Dimitry Kolyshev Date: Mon Sep 9 12:05:23 2024 +0700 cmd: imp code commit 46bd9e57360391e14edbdf83794cfb7dd570a8e3 Author: Dimitry Kolyshev Date: Mon Sep 9 11:52:12 2024 +0700 cmd: imp code commit 0f0df346b18c01dedf3c2e7820eb8cc043609f63 Author: Dimitry Kolyshev Date: Mon Sep 9 11:44:55 2024 +0700 cmd: imp code commit 21c7d22618d7952c215a7aeba7b0bf14104941bd Author: Dimitry Kolyshev Date: Mon Sep 9 11:11:46 2024 +0700 cmd: imp code commit 8978f068f26e95697279add23158eb727333396e Author: Dimitry Kolyshev Date: Mon Sep 9 11:00:38 2024 +0700 cmd: imp code commit f8a4e2ae5d5111e39bbfe5c79f6f63cf8f2c02e5 Author: Dimitry Kolyshev Date: Thu Sep 5 11:54:56 2024 +0800 cmd: imp code commit b86c63ea8f466d4d8e1b6c85d0d4a8989dacaad6 Author: Dimitry Kolyshev Date: Tue Sep 3 09:53:40 2024 +0300 cmd: flag slice structs commit 2f59748349af76575e84a4d64a8376d83cc0b13f Author: Dimitry Kolyshev Date: Fri Aug 30 16:56:04 2024 +0300 cmd: imp code commit 4b09664903d6adf4e9415adcb1e182d8802daa0a Author: Dimitry Kolyshev Date: Fri Aug 30 16:45:43 2024 +0300 cmd: imp code commit 87aa2f5cd23d3a9828fa990d33bf4955b5bc2420 Author: Dimitry Kolyshev Date: Fri Aug 30 09:18:24 2024 +0300 cmd: imp code commit 29736a106fae23142e82f88c97d6357c33559915 Author: Dimitry Kolyshev Date: Thu Aug 29 16:47:40 2024 +0300 cmd: imp code commit 8d125a47abb18ab17c092e549043f98b844ce570 Author: Dimitry Kolyshev Date: Thu Aug 29 11:24:10 2024 +0300 cmd: imp code commit 7c8b8695e720359ced1be9ff49a9c22f92297ade Author: Dimitry Kolyshev Date: Thu Aug 29 09:51:32 2024 +0300 cmd: help commit 123a85d25c6b9e1cc3bff8287a81899ef824b400 Author: Dimitry Kolyshev Date: Thu Aug 29 09:25:18 2024 +0300 cmd: opts merge ... and 8 more commits --- go.mod | 1 - go.sum | 2 - internal/cmd/args.go | 582 ++++++++++++++++++++++++++++++++++++++ internal/cmd/cmd.go | 22 +- internal/cmd/config.go | 610 ++++++++++++---------------------------- internal/cmd/flag.go | 158 +++++++++++ internal/cmd/options.go | 346 ----------------------- internal/cmd/proxy.go | 528 ++++++++++++++++++++++++++++++++++ internal/cmd/tls.go | 8 +- 9 files changed, 1457 insertions(+), 800 deletions(-) create mode 100644 internal/cmd/args.go create mode 100644 internal/cmd/flag.go delete mode 100644 internal/cmd/options.go create mode 100644 internal/cmd/proxy.go diff --git a/go.mod b/go.mod index b71655e23..1a648e1d1 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/ameshkov/dnsstamps v1.0.3 github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 github.com/bluele/gcache v0.0.2 - github.com/jessevdk/go-flags v1.6.1 github.com/miekg/dns v1.1.58 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/quic-go/quic-go v0.44.0 diff --git a/go.sum b/go.sum index dce68d906..c83435622 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,6 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20240130152714-0ed6a68c8d9e h1:E+3PBMCXn0ma79O7iCrne0iUpKtZ7rIcZvoz+jNtNtw= github.com/google/pprof v0.0.0-20240130152714-0ed6a68c8d9e/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= -github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= diff --git a/internal/cmd/args.go b/internal/cmd/args.go new file mode 100644 index 000000000..4626fef1a --- /dev/null +++ b/internal/cmd/args.go @@ -0,0 +1,582 @@ +package cmd + +import ( + "flag" + "fmt" + "io" + "os" + "slices" + "strings" + + "github.com/AdguardTeam/dnsproxy/internal/version" + "github.com/AdguardTeam/golibs/errors" + "github.com/AdguardTeam/golibs/osutil" + "github.com/AdguardTeam/golibs/timeutil" +) + +// Indexes to help with the [commandLineOptions] initialization. +const ( + configPathIdx = iota + logOutputIdx + tlsCertPathIdx + tlsKeyPathIdx + httpsServerNameIdx + httpsUserinfoIdx + dnsCryptConfigPathIdx + ednsAddrIdx + upstreamModeIdx + listenAddrsIdx + listenPortsIdx + httpsListenPortsIdx + tlsListenPortsIdx + quicListenPortsIdx + dnsCryptListenPortsIdx + upstreamsIdx + bootstrapDNSIdx + fallbacksIdx + privateRDNSUpstreamsIdx + dns64PrefixIdx + privateSubnetsIdx + bogusNXDomainIdx + hostsFilesIdx + timeoutIdx + cacheMinTTLIdx + cacheMaxTTLIdx + cacheSizeBytesIdx + ratelimitIdx + ratelimitSubnetLenIPv4Idx + ratelimitSubnetLenIPv6Idx + udpBufferSizeIdx + maxGoRoutinesIdx + tlsMinVersionIdx + tlsMaxVersionIdx + helpIdx + hostsFileEnabledIdx + pprofIdx + versionIdx + verboseIdx + insecureIdx + ipv6DisabledIdx + http3Idx + cacheOptimisticIdx + cacheIdx + refuseAnyIdx + enableEDNSSubnetIdx + dns64Idx + usePrivateRDNSIdx +) + +// commandLineOption contains information about a command-line option: its long +// and, if there is one, short forms, the value type, and the description. +type commandLineOption struct { + description string + long string + short string + valueType string +} + +// commandLineOptions are all command-line options currently supported by the +// binary. +var commandLineOptions = []*commandLineOption{ + configPathIdx: { + description: "YAML configuration file. Minimal working configuration in config.yaml.dist." + + " Options passed through command line will override the ones from this file.", + long: "config-path", + short: "", + valueType: "path", + }, + logOutputIdx: { + description: `Path to the log file.`, + long: "output", + short: "o", + valueType: "path", + }, + tlsCertPathIdx: { + description: "Path to a file with the certificate chain.", + long: "tls-crt", + short: "c", + valueType: "path", + }, + tlsKeyPathIdx: { + description: "Path to a file with the private key.", + long: "tls-key", + short: "k", + valueType: "path", + }, + httpsServerNameIdx: { + description: "Set the Server header for the responses from the HTTPS server.", + long: "https-server-name", + short: "", + valueType: "name", + }, + httpsUserinfoIdx: { + description: "If set, all DoH queries are required to have this basic authentication " + + "information.", + long: "https-userinfo", + short: "", + valueType: "name", + }, + dnsCryptConfigPathIdx: { + description: "Path to a file with DNSCrypt configuration. You can generate one using " + + "https://github.com/ameshkov/dnscrypt.", + long: "dnscrypt-config", + short: "g", + valueType: "path", + }, + ednsAddrIdx: { + description: "Send EDNS Client Address.", + long: "edns-addr", + short: "", + valueType: "address", + }, + upstreamModeIdx: { + description: "Defines the upstreams logic mode, possible values: load_balance, parallel, " + + "fastest_addr (default: load_balance).", + long: "upstream-mode", + short: "", + valueType: "mode", + }, + listenAddrsIdx: { + description: "Listening addresses.", + long: "listen", + short: "l", + valueType: "address", + }, + listenPortsIdx: { + description: "Listening ports. Zero value disables TCP and UDP listeners.", + long: "port", + short: "p", + valueType: "port", + }, + httpsListenPortsIdx: { + description: "Listening ports for DNS-over-HTTPS.", + long: "https-port", + short: "s", + valueType: "port", + }, + tlsListenPortsIdx: { + description: "Listening ports for DNS-over-TLS.", + long: "tls-port", + short: "t", + valueType: "port", + }, + quicListenPortsIdx: { + description: "Listening ports for DNS-over-QUIC.", + long: "quic-port", + short: "q", + valueType: "port", + }, + dnsCryptListenPortsIdx: { + description: "Listening ports for DNSCrypt.", + long: "dnscrypt-port", + short: "y", + valueType: "port", + }, + upstreamsIdx: { + description: "An upstream to be used (can be specified multiple times). You can also " + + "specify path to a file with the list of servers.", + long: "upstream", + short: "u", + valueType: "", + }, + bootstrapDNSIdx: { + description: "Bootstrap DNS for DoH and DoT, can be specified multiple times (default: " + + "use system-provided).", + long: "bootstrap", + short: "b", + valueType: "", + }, + fallbacksIdx: { + description: "Fallback resolvers to use when regular ones are unavailable, can be " + + "specified multiple times. You can also specify path to a file with the list of servers.", + long: "fallback", + short: "f", + valueType: "", + }, + privateRDNSUpstreamsIdx: { + description: "Private DNS upstreams to use for reverse DNS lookups of private addresses, " + + "can be specified multiple times.", + long: "private-rdns-upstream", + short: "", + valueType: "", + }, + dns64PrefixIdx: { + description: "Prefix used to handle DNS64. If not specified, dnsproxy uses the " + + "'Well-Known Prefix' 64:ff9b::. Can be specified multiple times.", + long: "dns64-prefix", + short: "", + valueType: "subnet", + }, + privateSubnetsIdx: { + description: "Private subnets to use for reverse DNS lookups of private addresses.", + long: "private-subnets", + short: "", + valueType: "subnet", + }, + bogusNXDomainIdx: { + description: "Transform the responses containing at least a single IP that matches " + + "specified addresses and CIDRs into NXDOMAIN. Can be specified multiple times.", + long: "bogus-nxdomain", + short: "", + valueType: "subnet", + }, + hostsFilesIdx: { + description: "List of paths to the hosts files relative to the root, can be specified " + + "multiple times.", + long: "hosts-files", + short: "", + valueType: "path", + }, + timeoutIdx: { + description: "Timeout for outbound DNS queries to remote upstream servers in a " + + "human-readable form", + long: "timeout", + short: "", + valueType: "duration", + }, + cacheMinTTLIdx: { + description: "Minimum TTL value for DNS entries, in seconds. Capped at 3600. " + + "Artificially extending TTLs should only be done with careful consideration.", + long: "cache-min-ttl", + short: "", + valueType: "uint32", + }, + cacheMaxTTLIdx: { + description: "Maximum TTL value for DNS entries, in seconds.", + long: "cache-max-ttl", + short: "", + valueType: "uint32", + }, + cacheSizeBytesIdx: { + description: "Cache size (in bytes). Default: 64k.", + long: "cache-size", + short: "", + valueType: "int", + }, + ratelimitIdx: { + description: "Ratelimit (requests per second).", + long: "ratelimit", + short: "r", + valueType: "int", + }, + ratelimitSubnetLenIPv4Idx: { + description: "Ratelimit subnet length for IPv4.", + long: "ratelimit-subnet-len-ipv4", + short: "", + valueType: "int", + }, + ratelimitSubnetLenIPv6Idx: { + description: "Ratelimit subnet length for IPv6.", + long: "ratelimit-subnet-len-ipv6", + short: "", + valueType: "int", + }, + udpBufferSizeIdx: { + description: "Set the size of the UDP buffer in bytes. A value <= 0 will use the system " + + "default.", + long: "udp-buf-size", + short: "", + valueType: "int", + }, + maxGoRoutinesIdx: { + description: "Set the maximum number of go routines. A zero value will not not set a " + + "maximum.", + long: "max-go-routines", + short: "", + valueType: "uint", + }, + tlsMinVersionIdx: { + description: "Minimum TLS version, for example 1.0.", + long: "tls-min-version", + short: "", + valueType: "version", + }, + tlsMaxVersionIdx: { + description: "Maximum TLS version, for example 1.3.", + long: "tls-max-version", + short: "", + valueType: "version", + }, + helpIdx: { + description: "Print this help message and quit.", + long: "help", + short: "h", + valueType: "", + }, + hostsFileEnabledIdx: { + description: "If specified, use hosts files for resolving.", + long: "hosts-file-enabled", + short: "", + valueType: "", + }, + pprofIdx: { + description: "If present, exposes pprof information on localhost:6060.", + long: "pprof", + short: "", + valueType: "", + }, + versionIdx: { + description: "Prints the program version.", + long: "version", + short: "", + valueType: "", + }, + verboseIdx: { + description: "Verbose output.", + long: "verbose", + short: "v", + valueType: "", + }, + insecureIdx: { + description: "Disable secure TLS certificate validation.", + long: "insecure", + short: "", + valueType: "", + }, + ipv6DisabledIdx: { + description: "If specified, all AAAA requests will be replied with NoError RCode and " + + "empty answer.", + long: "ipv6-disabled", + short: "", + valueType: "", + }, + http3Idx: { + description: "Enable HTTP/3 support.", + long: "http3", + short: "", + valueType: "", + }, + cacheOptimisticIdx: { + description: "If specified, optimistic DNS cache is enabled.", + long: "cache-optimistic", + short: "", + valueType: "", + }, + cacheIdx: { + description: "If specified, DNS cache is enabled.", + long: "cache", + short: "", + valueType: "", + }, + refuseAnyIdx: { + description: "If specified, refuses ANY requests.", + long: "refuse-any", + short: "", + valueType: "", + }, + enableEDNSSubnetIdx: { + description: "Use EDNS Client Subnet extension.", + long: "edns", + short: "", + valueType: "", + }, + dns64Idx: { + description: "If specified, dnsproxy will act as a DNS64 server.", + long: "dns64", + short: "", + valueType: "", + }, + usePrivateRDNSIdx: { + description: "If specified, use private upstreams for reverse DNS lookups of private " + + "addresses.", + long: "use-private-rdns", + short: "", + valueType: "", + }, +} + +// parseCmdLineOptions parses the command-line options. conf must not be nil. +func parseCmdLineOptions(conf *configuration) (err error) { + cmdName, args := os.Args[0], os.Args[1:] + + flags := flag.NewFlagSet(cmdName, flag.ContinueOnError) + for i, fieldPtr := range []any{ + configPathIdx: &conf.ConfigPath, + logOutputIdx: &conf.LogOutput, + tlsCertPathIdx: &conf.TLSCertPath, + tlsKeyPathIdx: &conf.TLSKeyPath, + httpsServerNameIdx: &conf.HTTPSServerName, + httpsUserinfoIdx: &conf.HTTPSUserinfo, + dnsCryptConfigPathIdx: &conf.DNSCryptConfigPath, + ednsAddrIdx: &conf.EDNSAddr, + upstreamModeIdx: &conf.UpstreamMode, + listenAddrsIdx: &conf.ListenAddrs, + listenPortsIdx: &conf.ListenPorts, + httpsListenPortsIdx: &conf.HTTPSListenPorts, + tlsListenPortsIdx: &conf.TLSListenPorts, + quicListenPortsIdx: &conf.QUICListenPorts, + dnsCryptListenPortsIdx: &conf.DNSCryptListenPorts, + upstreamsIdx: &conf.Upstreams, + bootstrapDNSIdx: &conf.BootstrapDNS, + fallbacksIdx: &conf.Fallbacks, + privateRDNSUpstreamsIdx: &conf.PrivateRDNSUpstreams, + dns64PrefixIdx: &conf.DNS64Prefix, + privateSubnetsIdx: &conf.PrivateSubnets, + bogusNXDomainIdx: &conf.BogusNXDomain, + hostsFilesIdx: &conf.HostsFiles, + timeoutIdx: &conf.Timeout, + cacheMinTTLIdx: &conf.CacheMinTTL, + cacheMaxTTLIdx: &conf.CacheMaxTTL, + cacheSizeBytesIdx: &conf.CacheSizeBytes, + ratelimitIdx: &conf.Ratelimit, + ratelimitSubnetLenIPv4Idx: &conf.RatelimitSubnetLenIPv4, + ratelimitSubnetLenIPv6Idx: &conf.RatelimitSubnetLenIPv6, + udpBufferSizeIdx: &conf.UDPBufferSize, + maxGoRoutinesIdx: &conf.MaxGoRoutines, + tlsMinVersionIdx: &conf.TLSMinVersion, + tlsMaxVersionIdx: &conf.TLSMaxVersion, + helpIdx: &conf.help, + hostsFileEnabledIdx: &conf.HostsFileEnabled, + pprofIdx: &conf.Pprof, + versionIdx: &conf.Version, + verboseIdx: &conf.Verbose, + insecureIdx: &conf.Insecure, + ipv6DisabledIdx: &conf.IPv6Disabled, + http3Idx: &conf.HTTP3, + cacheOptimisticIdx: &conf.CacheOptimistic, + cacheIdx: &conf.Cache, + refuseAnyIdx: &conf.RefuseAny, + enableEDNSSubnetIdx: &conf.EnableEDNSSubnet, + dns64Idx: &conf.DNS64, + usePrivateRDNSIdx: &conf.UsePrivateRDNS, + } { + addOption(flags, fieldPtr, commandLineOptions[i]) + } + + flags.Usage = func() { usage(cmdName, os.Stderr) } + + err = flags.Parse(args) + if err != nil { + // Don't wrap the error, because it's informative enough as is. + return err + } + + return nil +} + +// defineFlag defines a flag with specified setFlag function. o must not be +// nil. +func defineFlag[T any]( + fieldPtr *T, + o *commandLineOption, + setFlag func(p *T, name string, value T, usage string), +) { + setFlag(fieldPtr, o.long, *fieldPtr, o.description) + if o.short != "" { + setFlag(fieldPtr, o.short, *fieldPtr, o.description) + } +} + +// defineFlagVar defines a flag with the specified [flag.Value] value. o must +// not be nil. +func defineFlagVar(flags *flag.FlagSet, value flag.Value, o *commandLineOption) { + flags.Var(value, o.long, o.description) + if o.short != "" { + flags.Var(value, o.short, o.description) + } +} + +// defineTimeutilDurationFlag defines a flag with for the specified +// [*timeutil.Duration] pointer and command line option. o must not be nil. +func defineTimeutilDurationFlag( + flags *flag.FlagSet, + fieldPtr *timeutil.Duration, + o *commandLineOption, +) { + flags.TextVar(fieldPtr, o.long, *fieldPtr, o.description) + if o.short != "" { + flags.TextVar(fieldPtr, o.short, *fieldPtr, o.description) + } +} + +// addOption adds the command-line option described by o to flags using fieldPtr +// as the pointer to the value. +func addOption(flags *flag.FlagSet, fieldPtr any, o *commandLineOption) { + switch fieldPtr := fieldPtr.(type) { + case *string: + defineFlag(fieldPtr, o, flags.StringVar) + case *bool: + defineFlag(fieldPtr, o, flags.BoolVar) + case *int: + defineFlag(fieldPtr, o, flags.IntVar) + case *uint: + defineFlag(fieldPtr, o, flags.UintVar) + case *uint32: + defineFlagVar(flags, (*uint32Value)(fieldPtr), o) + case *float32: + defineFlagVar(flags, (*float32Value)(fieldPtr), o) + case *[]int: + defineFlagVar(flags, newIntSliceValue(fieldPtr), o) + case *[]string: + defineFlagVar(flags, newStringSliceValue(fieldPtr), o) + case *timeutil.Duration: + defineTimeutilDurationFlag(flags, fieldPtr, o) + default: + panic(fmt.Errorf("unexpected field pointer type %T: %w", fieldPtr, errors.ErrBadEnumValue)) + } +} + +// usage prints a usage message similar to the one printed by package flag but +// taking long vs. short versions into account as well as using more informative +// value hints. +func usage(cmdName string, output io.Writer) { + options := slices.Clone(commandLineOptions) + slices.SortStableFunc(options, func(a, b *commandLineOption) (res int) { + return strings.Compare(a.long, b.long) + }) + + b := &strings.Builder{} + _, _ = fmt.Fprintf(b, "Usage of %s:\n", cmdName) + + for _, o := range options { + writeUsageLine(b, o) + + // Use four spaces before the tab to trigger good alignment for both 4- + // and 8-space tab stops. + _, _ = fmt.Fprintf(b, " \t%s\n", o.description) + } + + _, _ = io.WriteString(output, b.String()) +} + +// writeUsageLine writes the usage line for the provided command-line option. +func writeUsageLine(b *strings.Builder, o *commandLineOption) { + if o.short == "" { + if o.valueType == "" { + _, _ = fmt.Fprintf(b, " --%s\n", o.long) + } else { + _, _ = fmt.Fprintf(b, " --%s=%s\n", o.long, o.valueType) + } + + return + } + + if o.valueType == "" { + _, _ = fmt.Fprintf(b, " --%s/-%s\n", o.long, o.short) + } else { + _, _ = fmt.Fprintf(b, " --%[1]s=%[3]s/-%[2]s %[3]s\n", o.long, o.short, o.valueType) + } +} + +// processCmdLineOptions decides if dnsproxy should exit depending on the +// results of command-line option parsing. +func processCmdLineOptions(conf *configuration, parseErr error) (exitCode int, needExit bool) { + if parseErr != nil { + // Assume that usage has already been printed. + return osutil.ExitCodeArgumentError, true + } + + if conf.help { + usage(os.Args[0], os.Stdout) + + return osutil.ExitCodeSuccess, true + } + + if conf.Version { + fmt.Printf("dnsproxy version %s\n", version.Version()) + + return osutil.ExitCodeSuccess, true + } + + return osutil.ExitCodeSuccess, false +} diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 957d39650..a9f505e02 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -22,20 +22,20 @@ import ( // Main is the entrypoint of dnsproxy CLI. Main may accept arguments, such as // embedded assets and command-line arguments. func Main() { - opts, exitCode, err := parseOptions() + conf, exitCode, err := parseConfig() if err != nil { - _, _ = fmt.Fprintln(os.Stderr, err) + _, _ = fmt.Fprintln(os.Stderr, fmt.Errorf("parsing options: %w", err)) } - if opts == nil { + if conf == nil { os.Exit(exitCode) } logOutput := os.Stdout - if opts.LogOutput != "" { + if conf.LogOutput != "" { // #nosec G302 -- Trust the file path that is given in the // configuration. - logOutput, err = os.OpenFile(opts.LogOutput, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644) + logOutput, err = os.OpenFile(conf.LogOutput, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644) if err != nil { _, _ = fmt.Fprintln(os.Stderr, fmt.Errorf("cannot create a log file: %s", err)) @@ -50,16 +50,16 @@ func Main() { Format: slogutil.FormatDefault, // TODO(d.kolyshev): Consider making configurable. AddTimestamp: true, - Verbose: opts.Verbose, + Verbose: conf.Verbose, }) ctx := context.Background() - if opts.Pprof { + if conf.Pprof { runPprof(l) } - err = runProxy(ctx, l, opts) + err = runProxy(ctx, l, conf) if err != nil { l.ErrorContext(ctx, "running dnsproxy", slogutil.KeyError, err) @@ -77,7 +77,7 @@ func Main() { // runProxy starts and runs the proxy. l must not be nil. // // TODO(e.burkov): Move into separate dnssvc package. -func runProxy(ctx context.Context, l *slog.Logger, options *Options) (err error) { +func runProxy(ctx context.Context, l *slog.Logger, conf *configuration) (err error) { var ( buildVersion = version.Version() revision = version.Revision() @@ -95,12 +95,12 @@ func runProxy(ctx context.Context, l *slog.Logger, options *Options) (err error) ) // Prepare the proxy server and its configuration. - conf, err := createProxyConfig(ctx, l, options) + proxyConf, err := createProxyConfig(ctx, l, conf) if err != nil { return fmt.Errorf("configuring proxy: %w", err) } - dnsProxy, err := proxy.New(conf) + dnsProxy, err := proxy.New(proxyConf) if err != nil { return fmt.Errorf("creating proxy: %w", err) } diff --git a/internal/cmd/config.go b/internal/cmd/config.go index cefdb2a7f..c9418d7e6 100644 --- a/internal/cmd/config.go +++ b/internal/cmd/config.go @@ -1,515 +1,253 @@ package cmd import ( - "context" - "crypto/tls" "fmt" - "log/slog" - "net" - "net/netip" - "net/url" "os" - "strings" + "time" - "github.com/AdguardTeam/dnsproxy/internal/dnsmsg" - "github.com/AdguardTeam/dnsproxy/internal/handler" - proxynetutil "github.com/AdguardTeam/dnsproxy/internal/netutil" "github.com/AdguardTeam/dnsproxy/proxy" - "github.com/AdguardTeam/dnsproxy/upstream" - "github.com/AdguardTeam/golibs/errors" - "github.com/AdguardTeam/golibs/logutil/slogutil" - "github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/osutil" - "github.com/ameshkov/dnscrypt/v2" + "github.com/AdguardTeam/golibs/timeutil" "gopkg.in/yaml.v3" ) -// TODO(e.burkov): Use a separate type for the YAML configuration file. +// configuration represents dnsproxy configuration. +type configuration struct { + // ConfigPath is the path to the configuration file. + ConfigPath string -// parseConfigFile fills options with the settings from file read by the given -// path. -func parseConfigFile(options *Options, confPath string) (err error) { - // #nosec G304 -- Trust the file path that is given in the args. - b, err := os.ReadFile(confPath) - if err != nil { - return fmt.Errorf("reading file: %w", err) - } + // LogOutput is the path to the log file. + LogOutput string `yaml:"output"` - err = yaml.Unmarshal(b, options) - if err != nil { - return fmt.Errorf("unmarshalling file: %w", err) - } + // TLSCertPath is the path to the .crt with the certificate chain. + TLSCertPath string `yaml:"tls-crt"` - return nil -} + // TLSKeyPath is the path to the file with the private key. + TLSKeyPath string `yaml:"tls-key"` -// createProxyConfig initializes [proxy.Config]. l must not be nil. -func createProxyConfig( - ctx context.Context, - l *slog.Logger, - options *Options, -) (conf *proxy.Config, err error) { - hostsFiles, err := options.hostsFiles(ctx, l) - if err != nil { - // Don't wrap the error since it's informative enough as is. - return nil, err - } + // HTTPSServerName sets Server header for the HTTPS server. + HTTPSServerName string `yaml:"https-server-name"` - reqHdlr, err := handler.NewDefault(&handler.DefaultConfig{ - Logger: l.With(slogutil.KeyPrefix, "default_handler"), - // TODO(e.burkov): Use the configured message constructor. - MessageConstructor: dnsmsg.DefaultMessageConstructor{}, - HaltIPv6: options.IPv6Disabled, - HostsFiles: hostsFiles, - FileSystem: osutil.RootDirFS(), - }) - if err != nil { - return nil, fmt.Errorf("creating default handler: %w", err) - } + // HTTPSUserinfo is the sole permitted userinfo for the DoH basic + // authentication. If it is set, all DoH queries are required to have this + // basic authentication information. + HTTPSUserinfo string `yaml:"https-userinfo"` - conf = &proxy.Config{ - Logger: l.With(slogutil.KeyPrefix, proxy.LogPrefix), - - RatelimitSubnetLenIPv4: options.RatelimitSubnetLenIPv4, - RatelimitSubnetLenIPv6: options.RatelimitSubnetLenIPv6, - - Ratelimit: options.Ratelimit, - CacheEnabled: options.Cache, - CacheSizeBytes: options.CacheSizeBytes, - CacheMinTTL: options.CacheMinTTL, - CacheMaxTTL: options.CacheMaxTTL, - CacheOptimistic: options.CacheOptimistic, - RefuseAny: options.RefuseAny, - HTTP3: options.HTTP3, - // TODO(e.burkov): The following CIDRs are aimed to match any address. - // This is not quite proper approach to be used by default so think - // about configuring it. - TrustedProxies: netutil.SliceSubnetSet{ - netip.MustParsePrefix("0.0.0.0/0"), - netip.MustParsePrefix("::0/0"), - }, - EnableEDNSClientSubnet: options.EnableEDNSSubnet, - UDPBufferSize: options.UDPBufferSize, - HTTPSServerName: options.HTTPSServerName, - MaxGoroutines: options.MaxGoRoutines, - UsePrivateRDNS: options.UsePrivateRDNS, - PrivateSubnets: netutil.SubnetSetFunc(netutil.IsLocallyServed), - RequestHandler: reqHdlr.HandleRequest, - } + // DNSCryptConfigPath is the path to the DNSCrypt configuration file. + DNSCryptConfigPath string `yaml:"dnscrypt-config"` - if uiStr := options.HTTPSUserinfo; uiStr != "" { - user, pass, ok := strings.Cut(uiStr, ":") - if ok { - conf.Userinfo = url.UserPassword(user, pass) - } else { - conf.Userinfo = url.User(user) - } - } + // EDNSAddr is the custom EDNS Client Address to send. + EDNSAddr string `yaml:"edns-addr"` - options.initBogusNXDomain(ctx, l, conf) + // UpstreamMode determines the logic through which upstreams will be used. + // If not specified the [proxy.UpstreamModeLoadBalance] is used. + UpstreamMode string `yaml:"upstream-mode"` - var errs []error - errs = append(errs, options.initUpstreams(ctx, l, conf)) - errs = append(errs, options.initEDNS(ctx, l, conf)) - errs = append(errs, options.initTLSConfig(conf)) - errs = append(errs, options.initDNSCryptConfig(conf)) - errs = append(errs, options.initListenAddrs(conf)) - errs = append(errs, options.initSubnets(conf)) + // ListenAddrs is the list of server's listen addresses. + ListenAddrs []string `yaml:"listen-addrs"` - return conf, errors.Join(errs...) -} + // ListenPorts are the ports server listens on. + ListenPorts []int `yaml:"listen-ports"` -// isEmpty returns false if uc contains at least a single upstream. uc must not -// be nil. -// -// TODO(e.burkov): Think of a better way to validate the config. Perhaps, -// return an error from [ParseUpstreamsConfig] if no upstreams were initialized. -func isEmpty(uc *proxy.UpstreamConfig) (ok bool) { - return len(uc.Upstreams) == 0 && - len(uc.DomainReservedUpstreams) == 0 && - len(uc.SpecifiedDomainUpstreams) == 0 -} + // HTTPSListenPorts are the ports server listens on for DNS-over-HTTPS. + HTTPSListenPorts []int `yaml:"https-port"` -// initUpstreams inits upstream-related config fields. -// -// TODO(d.kolyshev): Join errors. -func (opts *Options) initUpstreams( - ctx context.Context, - l *slog.Logger, - config *proxy.Config, -) (err error) { - httpVersions := upstream.DefaultHTTPVersions - if opts.HTTP3 { - httpVersions = []upstream.HTTPVersion{ - upstream.HTTPVersion3, - upstream.HTTPVersion2, - upstream.HTTPVersion11, - } - } + // TLSListenPorts are the ports server listens on for DNS-over-TLS. + TLSListenPorts []int `yaml:"tls-port"` - timeout := opts.Timeout.Duration - bootOpts := &upstream.Options{ - Logger: l, - HTTPVersions: httpVersions, - InsecureSkipVerify: opts.Insecure, - Timeout: timeout.Duration, - } - boot, err := initBootstrap(ctx, l, opts.BootstrapDNS, bootOpts) - if err != nil { - return fmt.Errorf("initializing bootstrap: %w", err) - } + // QUICListenPorts are the ports server listens on for DNS-over-QUIC. + QUICListenPorts []int `yaml:"quic-port"` - upsOpts := &upstream.Options{ - Logger: l, - HTTPVersions: httpVersions, - InsecureSkipVerify: opts.Insecure, - Bootstrap: boot, - Timeout: timeout.Duration, - } - upstreams := loadServersList(opts.Upstreams) + // DNSCryptListenPorts are the ports server listens on for DNSCrypt. + DNSCryptListenPorts []int `yaml:"dnscrypt-port"` - config.UpstreamConfig, err = proxy.ParseUpstreamsConfig(upstreams, upsOpts) - if err != nil { - return fmt.Errorf("parsing upstreams configuration: %w", err) - } + // Upstreams is the list of DNS upstream servers. + Upstreams []string `yaml:"upstream"` - privateUpsOpts := &upstream.Options{ - Logger: l, - HTTPVersions: httpVersions, - Bootstrap: boot, - Timeout: min(defaultLocalTimeout, timeout.Duration), - } - privateUpstreams := loadServersList(opts.PrivateRDNSUpstreams) + // BootstrapDNS is the list of bootstrap DNS upstream servers. + BootstrapDNS []string `yaml:"bootstrap"` - private, err := proxy.ParseUpstreamsConfig(privateUpstreams, privateUpsOpts) - if err != nil { - return fmt.Errorf("parsing private rdns upstreams configuration: %w", err) - } + // Fallbacks is the list of fallback DNS upstream servers. + Fallbacks []string `yaml:"fallback"` - if !isEmpty(private) { - config.PrivateRDNSUpstreamConfig = private - } + // PrivateRDNSUpstreams are upstreams to use for reverse DNS lookups of + // private addresses, including the requests for authority records, such as + // SOA and NS. + PrivateRDNSUpstreams []string `yaml:"private-rdns-upstream"` - fallbackUpstreams := loadServersList(opts.Fallbacks) - fallbacks, err := proxy.ParseUpstreamsConfig(fallbackUpstreams, upsOpts) - if err != nil { - return fmt.Errorf("parsing fallback upstreams configuration: %w", err) - } + // DNS64Prefix defines the DNS64 prefixes that dnsproxy should use when it + // acts as a DNS64 server. If not specified, dnsproxy uses the default + // Well-Known Prefix. This option can be specified multiple times. + DNS64Prefix []string `yaml:"dns64-prefix"` - if !isEmpty(fallbacks) { - config.Fallbacks = fallbacks - } + // PrivateSubnets is the list of private subnets to determine private + // addresses. + PrivateSubnets []string `yaml:"private-subnets"` - if opts.UpstreamMode != "" { - err = config.UpstreamMode.UnmarshalText([]byte(opts.UpstreamMode)) - if err != nil { - return fmt.Errorf("parsing upstream mode: %w", err) - } + // BogusNXDomain transforms responses that contain at least one of the given + // IP addresses into NXDOMAIN. + // + // TODO(a.garipov): Find a way to use [netutil.Prefix]. Currently, package + // go-flags doesn't support text unmarshalers. + BogusNXDomain []string `yaml:"bogus-nxdomain"` - return nil - } + // HostsFiles is the list of paths to the hosts files to resolve from. + HostsFiles []string `yaml:"hosts-files"` - config.UpstreamMode = proxy.UpstreamModeLoadBalance + // Timeout for outbound DNS queries to remote upstream servers in a + // human-readable form. Default is 10s. + Timeout timeutil.Duration `yaml:"timeout"` - return nil -} + // CacheMinTTL is the minimum TTL value for caching DNS entries, in seconds. + // It overrides the TTL value from the upstream server, if the one is less. + CacheMinTTL uint32 `yaml:"cache-min-ttl"` -// initBootstrap initializes the [upstream.Resolver] for bootstrapping upstream -// servers. It returns the default resolver if no bootstraps were specified. -// The returned resolver will also use system hosts files first. -func initBootstrap( - ctx context.Context, - l *slog.Logger, - bootstraps []string, - opts *upstream.Options, -) (r upstream.Resolver, err error) { - var resolvers []upstream.Resolver - - for i, b := range bootstraps { - var ur *upstream.UpstreamResolver - ur, err = upstream.NewUpstreamResolver(b, opts) - if err != nil { - return nil, fmt.Errorf("creating bootstrap resolver at index %d: %w", i, err) - } - - resolvers = append(resolvers, upstream.NewCachingResolver(ur)) - } + // CacheMaxTTL is the maximum TTL value for caching DNS entries, in seconds. + // It overrides the TTL value from the upstream server, if the one is + // greater. + CacheMaxTTL uint32 `yaml:"cache-max-ttl"` - switch len(resolvers) { - case 0: - etcHosts, hostsErr := upstream.NewDefaultHostsResolver(osutil.RootDirFS(), l) - if hostsErr != nil { - l.ErrorContext(ctx, "creating default hosts resolver", slogutil.KeyError, hostsErr) + // CacheSizeBytes is the cache size in bytes. Default is 64k. + CacheSizeBytes int `yaml:"cache-size"` - return net.DefaultResolver, nil - } + // Ratelimit is the maximum number of requests per second. + Ratelimit int `yaml:"ratelimit"` - return upstream.ConsequentResolver{etcHosts, net.DefaultResolver}, nil - case 1: - return resolvers[0], nil - default: - return upstream.ParallelResolver(resolvers), nil - } -} + // RatelimitSubnetLenIPv4 is a subnet length for IPv4 addresses used for + // rate limiting requests. + RatelimitSubnetLenIPv4 int `yaml:"ratelimit-subnet-len-ipv4"` -// initEDNS inits EDNS-related config fields. -func (opts *Options) initEDNS( - ctx context.Context, - l *slog.Logger, - config *proxy.Config, -) (err error) { - if opts.EDNSAddr == "" { - return nil - } + // RatelimitSubnetLenIPv6 is a subnet length for IPv6 addresses used for + // rate limiting requests. + RatelimitSubnetLenIPv6 int `yaml:"ratelimit-subnet-len-ipv6"` - if !opts.EnableEDNSSubnet { - l.WarnContext(ctx, "--edns is required", "--edns-addr", opts.EDNSAddr) + // UDPBufferSize is the size of the UDP buffer in bytes. A value <= 0 will + // use the system default. + UDPBufferSize int `yaml:"udp-buf-size"` - return nil - } + // MaxGoRoutines is the maximum number of goroutines. + MaxGoRoutines uint `yaml:"max-go-routines"` - config.EDNSAddr, err = netutil.ParseIP(opts.EDNSAddr) - if err != nil { - return fmt.Errorf("parsing edns-addr: %w", err) - } + // TLSMinVersion is the minimum allowed version of TLS. + // + // TODO(d.kolyshev): Use more suitable type. + TLSMinVersion float32 `yaml:"tls-min-version"` - return nil -} + // TLSMaxVersion is the maximum allowed version of TLS. + // + // TODO(d.kolyshev): Use more suitable type. + TLSMaxVersion float32 `yaml:"tls-max-version"` -// initBogusNXDomain inits BogusNXDomain structure. -func (opts *Options) initBogusNXDomain(ctx context.Context, l *slog.Logger, config *proxy.Config) { - if len(opts.BogusNXDomain) == 0 { - return - } + // help, if true, prints the command-line option help message and quit with + // a successful exit-code. + help bool - for i, s := range opts.BogusNXDomain { - p, err := proxynetutil.ParseSubnet(s) - if err != nil { - // TODO(a.garipov): Consider returning this err as a proper error. - l.WarnContext(ctx, "parsing bogus nxdomain", "index", i, slogutil.KeyError, err) - } else { - config.BogusNXDomain = append(config.BogusNXDomain, p) - } - } -} + // HostsFileEnabled controls whether hosts files are used for resolving or + // not. + HostsFileEnabled bool `yaml:"hosts-file-enabled"` -// initTLSConfig inits the TLS config. -func (opts *Options) initTLSConfig(config *proxy.Config) (err error) { - if opts.TLSCertPath != "" && opts.TLSKeyPath != "" { - var tlsConfig *tls.Config - tlsConfig, err = newTLSConfig(opts) - if err != nil { - return fmt.Errorf("loading TLS config: %w", err) - } + // Pprof defines whether the pprof information needs to be exposed via + // localhost:6060 or not. + Pprof bool `yaml:"pprof"` - config.TLSConfig = tlsConfig - } + // Version, if true, prints the program version, and exits. + Version bool `yaml:"version"` - return nil -} + // Verbose controls the verbosity of the output. + Verbose bool `yaml:"verbose"` -// initDNSCryptConfig inits the DNSCrypt config. -func (opts *Options) initDNSCryptConfig(config *proxy.Config) (err error) { - if opts.DNSCryptConfigPath == "" { - return - } + // Insecure disables upstream servers TLS certificate verification. + Insecure bool `yaml:"insecure"` - b, err := os.ReadFile(opts.DNSCryptConfigPath) - if err != nil { - return fmt.Errorf("reading DNSCrypt config %q: %w", opts.DNSCryptConfigPath, err) - } + // IPv6Disabled makes the server to respond with NODATA to all AAAA queries. + IPv6Disabled bool `yaml:"ipv6-disabled"` - rc := &dnscrypt.ResolverConfig{} - err = yaml.Unmarshal(b, rc) - if err != nil { - return fmt.Errorf("unmarshalling DNSCrypt config: %w", err) - } + // HTTP3 controls whether HTTP/3 is enabled for this instance of dnsproxy. + // It enables HTTP/3 support for both the DoH upstreams and the DoH server. + HTTP3 bool `yaml:"http3"` - cert, err := rc.CreateCert() - if err != nil { - return fmt.Errorf("creating DNSCrypt certificate: %w", err) - } + // CacheOptimistic, if set to true, enables the optimistic DNS cache. That + // means that cached results will be served even if their cache TTL has + // already expired. + CacheOptimistic bool `yaml:"cache-optimistic"` - config.DNSCryptResolverCert = cert - config.DNSCryptProviderName = rc.ProviderName + // Cache controls whether DNS responses are cached or not. + Cache bool `yaml:"cache"` - return nil -} + // RefuseAny makes the server to refuse requests of type ANY. + RefuseAny bool `yaml:"refuse-any"` -// parseListenAddrs returns a slice of listen IP addresses from the given -// options. In case no addresses are specified by options returns a slice with -// the IPv4 unspecified address "0.0.0.0". -// -// TODO(d.kolyshev): Join errors. -func parseListenAddrs(addrStrs []string) (addrs []netip.Addr, err error) { - for i, a := range addrStrs { - var ip netip.Addr - ip, err = netip.ParseAddr(a) - if err != nil { - return addrs, fmt.Errorf("parsing listen address at index %d: %s", i, a) - } - - addrs = append(addrs, ip) - } + // EnableEDNSSubnet uses EDNS Client Subnet extension. + EnableEDNSSubnet bool `yaml:"edns"` - if len(addrs) == 0 { - // If ListenAddrs has not been parsed through config file nor command - // line we set it to "0.0.0.0". - // - // TODO(a.garipov): Consider using localhost. - addrs = append(addrs, netip.IPv4Unspecified()) - } + // DNS64 defines whether DNS64 functionality is enabled or not. + DNS64 bool `yaml:"dns64"` - return addrs, nil + // UsePrivateRDNS makes the server to use private upstreams for reverse DNS + // lookups of private addresses, including the requests for authority + // records, such as SOA and NS. + UsePrivateRDNS bool `yaml:"use-private-rdns"` } -// initListenAddrs sets up proxy configuration listen IP addresses. -func (opts *Options) initListenAddrs(config *proxy.Config) (err error) { - addrs, err := parseListenAddrs(opts.ListenAddrs) - if err != nil { - return fmt.Errorf("parsing listen addresses: %w", err) +// parseConfig returns options parsed from the command args or config file. If +// no options have been parsed, it returns a suitable exit code and an error. +func parseConfig() (conf *configuration, exitCode int, err error) { + conf = &configuration{ + HTTPSServerName: "dnsproxy", + UpstreamMode: string(proxy.UpstreamModeLoadBalance), + CacheSizeBytes: 64 * 1024, + Timeout: timeutil.Duration{Duration: 10 * time.Second}, + RatelimitSubnetLenIPv4: 24, + RatelimitSubnetLenIPv6: 56, + HostsFileEnabled: true, } - if len(opts.ListenPorts) == 0 { - // If ListenPorts has not been parsed through config file nor command - // line we set it to 53. - opts.ListenPorts = []int{53} + err = parseCmdLineOptions(conf) + exitCode, needExit := processCmdLineOptions(conf, err) + if needExit { + return nil, exitCode, err } - for _, port := range opts.ListenPorts { - for _, ip := range addrs { - addrPort := netip.AddrPortFrom(ip, uint16(port)) - - config.UDPListenAddr = append(config.UDPListenAddr, net.UDPAddrFromAddrPort(addrPort)) - config.TCPListenAddr = append(config.TCPListenAddr, net.TCPAddrFromAddrPort(addrPort)) - } + confPath := conf.ConfigPath + if confPath == "" { + return conf, exitCode, nil } - initTLSListenAddrs(config, opts, addrs) - initDNSCryptListenAddrs(config, opts, addrs) - - return nil -} - -// initTLSListenAddrs sets up proxy configuration TLS listen addresses. -func initTLSListenAddrs(config *proxy.Config, options *Options, addrs []netip.Addr) { - if config.TLSConfig == nil { - return - } + // TODO(d.kolyshev): Bootstrap and use slog. + fmt.Printf("dnsproxy config path: %s\n", confPath) - for _, ip := range addrs { - for _, port := range options.TLSListenPorts { - a := net.TCPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16(port))) - config.TLSListenAddr = append(config.TLSListenAddr, a) - } - - for _, port := range options.HTTPSListenPorts { - a := net.TCPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16(port))) - config.HTTPSListenAddr = append(config.HTTPSListenAddr, a) - } - - for _, port := range options.QUICListenPorts { - a := net.UDPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16(port))) - config.QUICListenAddr = append(config.QUICListenAddr, a) - } - } -} - -// initDNSCryptListenAddrs sets up proxy configuration DNSCrypt listen -// addresses. -func initDNSCryptListenAddrs(config *proxy.Config, options *Options, addrs []netip.Addr) { - if config.DNSCryptResolverCert == nil || config.DNSCryptProviderName == "" { - return - } - - for _, port := range options.DNSCryptListenPorts { - p := uint16(port) - - for _, ip := range addrs { - addrPort := netip.AddrPortFrom(ip, p) - - tcp := net.TCPAddrFromAddrPort(addrPort) - config.DNSCryptTCPListenAddr = append(config.DNSCryptTCPListenAddr, tcp) - - udp := net.UDPAddrFromAddrPort(addrPort) - config.DNSCryptUDPListenAddr = append(config.DNSCryptUDPListenAddr, udp) - } - } -} - -// initSubnets sets the DNS64 configuration into conf. -// -// TODO(d.kolyshev): Join errors. -func (opts *Options) initSubnets(conf *proxy.Config) (err error) { - if conf.UseDNS64 = opts.DNS64; conf.UseDNS64 { - for i, p := range opts.DNS64Prefix { - var pref netip.Prefix - pref, err = netip.ParsePrefix(p) - if err != nil { - return fmt.Errorf("parsing dns64 prefix at index %d: %w", i, err) - } - - conf.DNS64Prefs = append(conf.DNS64Prefs, pref) - } + err = parseConfigFile(conf, confPath) + if err != nil { + return nil, osutil.ExitCodeFailure, fmt.Errorf( + "parsing config file %s: %w", + confPath, + err, + ) } - if !opts.UsePrivateRDNS { - return nil + // Parse command-line args again as it has priority over YAML config. + err = parseCmdLineOptions(conf) + if err != nil { + // Don't wrap the error, because it's informative enough as is. + return nil, osutil.ExitCodeFailure, err } - return opts.initPrivateSubnets(conf) + return conf, exitCode, nil } -// initSubnets sets the private subnets configuration into conf. -func (opts *Options) initPrivateSubnets(conf *proxy.Config) (err error) { - private := make([]netip.Prefix, 0, len(opts.PrivateSubnets)) - for i, p := range opts.PrivateSubnets { - var pref netip.Prefix - pref, err = netip.ParsePrefix(p) - if err != nil { - return fmt.Errorf("parsing private subnet at index %d: %w", i, err) - } - - private = append(private, pref) +// parseConfigFile fills options with the settings from file read by the given +// path. +func parseConfigFile(conf *configuration, confPath string) (err error) { + // #nosec G304 -- Trust the file path that is given in the args. + b, err := os.ReadFile(confPath) + if err != nil { + return fmt.Errorf("reading file: %w", err) } - if len(private) > 0 { - conf.PrivateSubnets = netutil.SliceSubnetSet(private) + err = yaml.Unmarshal(b, conf) + if err != nil { + return fmt.Errorf("unmarshalling file: %w", err) } return nil } - -// loadServersList loads a list of DNS servers from the specified list. The -// thing is that the user may specify either a server address or the path to a -// file with a list of addresses. This method takes care of it, it reads the -// file and loads servers from this file if needed. -func loadServersList(sources []string) []string { - var servers []string - - for _, source := range sources { - // #nosec G304 -- Trust the file path that is given in the - // configuration. - data, err := os.ReadFile(source) - if err != nil { - // Ignore errors, just consider it a server address and not a file. - servers = append(servers, source) - } - - lines := strings.Split(string(data), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - - // Ignore comments in the file. - if line == "" || - strings.HasPrefix(line, "!") || - strings.HasPrefix(line, "#") { - continue - } - - servers = append(servers, line) - } - } - - return servers -} diff --git a/internal/cmd/flag.go b/internal/cmd/flag.go new file mode 100644 index 000000000..10cc1321c --- /dev/null +++ b/internal/cmd/flag.go @@ -0,0 +1,158 @@ +package cmd + +import ( + "flag" + "fmt" + "strconv" + "strings" + + "github.com/AdguardTeam/golibs/stringutil" +) + +// uint32Value is an uint32 that can be defined as a flag for [flag.FlagSet]. +type uint32Value uint32 + +// type check +var _ flag.Value = (*uint32Value)(nil) + +// Set implements the [flag.Value] interface for *uint32Value. +func (i *uint32Value) Set(s string) (err error) { + v, err := strconv.ParseUint(s, 0, 32) + *i = uint32Value(v) + + return err +} + +// String implements the [flag.Value] interface for *uint32Value. +func (i *uint32Value) String() (out string) { + return strconv.FormatUint(uint64(*i), 10) +} + +// float32Value is an float32 that can be defined as a flag for [flag.FlagSet]. +type float32Value float32 + +// type check +var _ flag.Value = (*float32Value)(nil) + +// Set implements the [flag.Value] interface for *float32Value. +func (i *float32Value) Set(s string) (err error) { + v, err := strconv.ParseFloat(s, 32) + *i = float32Value(v) + + return err +} + +// String implements the [flag.Value] interface for *float32Value. +func (i *float32Value) String() (out string) { + return strconv.FormatFloat(float64(*i), 'f', 3, 32) +} + +// intSliceValue represent a struct with a slice of integers that can be defined +// as a flag for [flag.FlagSet]. +type intSliceValue struct { + // values is the pointer to a slice of integers to store parsed values. + values *[]int + + // isSet is false until the corresponding flag is met for the first time. + // When the flag is found, the default value is overwritten with zero value. + isSet bool +} + +// newIntSliceValue returns a pointer to intSliceValue with the given value. +func newIntSliceValue(p *[]int) (out *intSliceValue) { + return &intSliceValue{ + values: p, + isSet: false, + } +} + +// type check +var _ flag.Value = (*intSliceValue)(nil) + +// Set implements the [flag.Value] interface for *intSliceValue. +func (i *intSliceValue) Set(s string) (err error) { + v, err := strconv.Atoi(s) + if err != nil { + return fmt.Errorf("parsing integer slice arg %q: %w", s, err) + } + + if !i.isSet { + i.isSet = true + *i.values = []int{} + } + + *i.values = append(*i.values, v) + + return nil +} + +// String implements the [flag.Value] interface for *intSliceValue. +func (i *intSliceValue) String() (out string) { + if i == nil || i.values == nil { + return "" + } + + sb := &strings.Builder{} + for idx, v := range *i.values { + if idx > 0 { + stringutil.WriteToBuilder(sb, ",") + } + + stringutil.WriteToBuilder(sb, strconv.Itoa(v)) + } + + return sb.String() +} + +// stringSliceValue represent a struct with a slice of strings that can be +// defined as a flag for [flag.FlagSet]. +type stringSliceValue struct { + // values is the pointer to a slice of string to store parsed values. + values *[]string + + // isSet is false until the corresponding flag is met for the first time. + // When the flag is found, the default value is overwritten with zero value. + isSet bool +} + +// newStringSliceValue returns a pointer to stringSliceValue with the given +// value. +func newStringSliceValue(p *[]string) (out *stringSliceValue) { + return &stringSliceValue{ + values: p, + isSet: false, + } +} + +// type check +var _ flag.Value = (*stringSliceValue)(nil) + +// Set implements the [flag.Value] interface for *stringSliceValue. +func (i *stringSliceValue) Set(s string) (err error) { + if !i.isSet { + i.isSet = true + *i.values = []string{} + } + + *i.values = append(*i.values, s) + + return nil +} + +// String implements the [flag.Value] interface for *stringSliceValue. +func (i *stringSliceValue) String() (out string) { + if i == nil || i.values == nil { + return "" + } + + sb := &strings.Builder{} + for idx, v := range *i.values { + if idx > 0 { + stringutil.WriteToBuilder(sb, ",") + } + + stringutil.WriteToBuilder(sb, v) + } + + return sb.String() +} diff --git a/internal/cmd/options.go b/internal/cmd/options.go deleted file mode 100644 index acd837113..000000000 --- a/internal/cmd/options.go +++ /dev/null @@ -1,346 +0,0 @@ -package cmd - -import ( - "context" - "encoding" - "fmt" - "log/slog" - "os" - "strconv" - "strings" - "time" - - "github.com/AdguardTeam/dnsproxy/internal/version" - "github.com/AdguardTeam/golibs/hostsfile" - "github.com/AdguardTeam/golibs/osutil" - "github.com/AdguardTeam/golibs/timeutil" - goFlags "github.com/jessevdk/go-flags" -) - -const ( - defaultLocalTimeout = 1 * time.Second - - argConfigPath = "--config-path=" - argVersion = "--version" - argHostsEnabled = "--hosts-file-enabled" -) - -// decodableBool is a boolean that can be unmarshaled from a flags. -// -// TODO(e.burkov): This is a workaround for go-flags, see -// https://github.com/AdguardTeam/dnsproxy/issues/182. -type decodableBool struct { - value bool -} - -// type check -var _ goFlags.Unmarshaler = (*decodableBool)(nil) - -// UnmarshalFlag implements the [goFlags.Unmarshaler] interface for -// *decodableBool. -func (b *decodableBool) UnmarshalFlag(text string) (err error) { - b.value, err = strconv.ParseBool(text) - - return err -} - -// type check -var _ encoding.TextUnmarshaler = (*decodableBool)(nil) - -// UnmarshalText implements the [encoding.TextUnmarshaler] interface for -// *decodableBool. -func (b *decodableBool) UnmarshalText(text []byte) (err error) { - b.value, err = strconv.ParseBool(string(text)) - - return err -} - -// decodableDuration is a duration that can be unmarshaled from a flags. -// -// TODO(e.burkov): This is a workaround for go-flags, see -// https://github.com/AdguardTeam/dnsproxy/issues/182. -type decodableDuration struct { - // Duration is embedded to avoid reimplementing its UnmarshalText method. - timeutil.Duration -} - -// type check -var _ goFlags.Unmarshaler = (*decodableDuration)(nil) - -// UnmarshalFlag implements the [goFlags.Unmarshaler] interface for -// *decodableDuration. -func (d *decodableDuration) UnmarshalFlag(text string) (err error) { - return d.UnmarshalText([]byte(text)) -} - -// Options represents console arguments. For further additions, please do not -// use the default option since it will cause some problems when config files -// are used. -// -// TODO(a.garipov): Consider extracting conf blocks for better fieldalignment. -type Options struct { - // HostsFileEnabled controls whether hosts files are used for resolving or - // not. - HostsFileEnabled *decodableBool `yaml:"hosts-file-enabled" long:"hosts-file-enabled" description:"If specified, use hosts files for resolving" default:"true"` - - // Configuration file path (yaml), the config path should be read without - // using goFlags in order not to have default values overriding yaml - // options. - ConfigPath string `long:"config-path" description:"yaml configuration file. Minimal working configuration in config.yaml.dist. Options passed through command line will override the ones from this file." default:""` - - // LogOutput is the path to the log file. - LogOutput string `yaml:"output" short:"o" long:"output" description:"Path to the log file. If not set, write to stdout."` - - // TLSCertPath is the path to the .crt with the certificate chain. - TLSCertPath string `yaml:"tls-crt" short:"c" long:"tls-crt" description:"Path to a file with the certificate chain"` - - // TLSKeyPath is the path to the file with the private key. - TLSKeyPath string `yaml:"tls-key" short:"k" long:"tls-key" description:"Path to a file with the private key"` - - // HTTPSServerName sets Server header for the HTTPS server. - HTTPSServerName string `yaml:"https-server-name" long:"https-server-name" description:"Set the Server header for the responses from the HTTPS server." default:"dnsproxy"` - - // HTTPSUserinfo is the sole permitted userinfo for the DoH basic - // authentication. If it is set, all DoH queries are required to have this - // basic authentication information. - HTTPSUserinfo string `yaml:"https-userinfo" long:"https-userinfo" description:"If set, all DoH queries are required to have this basic authentication information."` - - // DNSCryptConfigPath is the path to the DNSCrypt configuration file. - DNSCryptConfigPath string `yaml:"dnscrypt-config" short:"g" long:"dnscrypt-config" description:"Path to a file with DNSCrypt configuration. You can generate one using https://github.com/ameshkov/dnscrypt"` - - // EDNSAddr is the custom EDNS Client Address to send. - EDNSAddr string `yaml:"edns-addr" long:"edns-addr" description:"Send EDNS Client Address"` - - // UpstreamMode determines the logic through which upstreams will be used. - // If not specified the [proxy.UpstreamModeLoadBalance] is used. - UpstreamMode string `yaml:"upstream-mode" long:"upstream-mode" description:"Defines the upstreams logic mode, possible values: load_balance, parallel, fastest_addr (default: load_balance)" optional:"yes" optional-value:"load_balance"` - - // ListenAddrs is the list of server's listen addresses. - ListenAddrs []string `yaml:"listen-addrs" short:"l" long:"listen" description:"Listening addresses"` - - // ListenPorts are the ports server listens on. - ListenPorts []int `yaml:"listen-ports" short:"p" long:"port" description:"Listening ports. Zero value disables TCP and UDP listeners"` - - // HTTPSListenPorts are the ports server listens on for DNS-over-HTTPS. - HTTPSListenPorts []int `yaml:"https-port" short:"s" long:"https-port" description:"Listening ports for DNS-over-HTTPS"` - - // TLSListenPorts are the ports server listens on for DNS-over-TLS. - TLSListenPorts []int `yaml:"tls-port" short:"t" long:"tls-port" description:"Listening ports for DNS-over-TLS"` - - // QUICListenPorts are the ports server listens on for DNS-over-QUIC. - QUICListenPorts []int `yaml:"quic-port" short:"q" long:"quic-port" description:"Listening ports for DNS-over-QUIC"` - - // DNSCryptListenPorts are the ports server listens on for DNSCrypt. - DNSCryptListenPorts []int `yaml:"dnscrypt-port" short:"y" long:"dnscrypt-port" description:"Listening ports for DNSCrypt"` - - // Upstreams is the list of DNS upstream servers. - Upstreams []string `yaml:"upstream" short:"u" long:"upstream" description:"An upstream to be used (can be specified multiple times). You can also specify path to a file with the list of servers" optional:"false"` - - // BootstrapDNS is the list of bootstrap DNS upstream servers. - BootstrapDNS []string `yaml:"bootstrap" short:"b" long:"bootstrap" description:"Bootstrap DNS for DoH and DoT, can be specified multiple times (default: use system-provided)"` - - // Fallbacks is the list of fallback DNS upstream servers. - Fallbacks []string `yaml:"fallback" short:"f" long:"fallback" description:"Fallback resolvers to use when regular ones are unavailable, can be specified multiple times. You can also specify path to a file with the list of servers"` - - // PrivateRDNSUpstreams are upstreams to use for reverse DNS lookups of - // private addresses, including the requests for authority records, such as - // SOA and NS. - PrivateRDNSUpstreams []string `yaml:"private-rdns-upstream" long:"private-rdns-upstream" description:"Private DNS upstreams to use for reverse DNS lookups of private addresses, can be specified multiple times"` - - // DNS64Prefix defines the DNS64 prefixes that dnsproxy should use when it - // acts as a DNS64 server. If not specified, dnsproxy uses the default - // Well-Known Prefix. This option can be specified multiple times. - DNS64Prefix []string `yaml:"dns64-prefix" long:"dns64-prefix" description:"Prefix used to handle DNS64. If not specified, dnsproxy uses the 'Well-Known Prefix' 64:ff9b::. Can be specified multiple times" required:"false"` - - // PrivateSubnets is the list of private subnets to determine private - // addresses. - PrivateSubnets []string `yaml:"private-subnets" long:"private-subnets" description:"Private subnets to use for reverse DNS lookups of private addresses" required:"false"` - - // BogusNXDomain transforms responses that contain at least one of the given - // IP addresses into NXDOMAIN. - // - // TODO(a.garipov): Find a way to use [netutil.Prefix]. Currently, package - // go-flags doesn't support text unmarshalers. - BogusNXDomain []string `yaml:"bogus-nxdomain" long:"bogus-nxdomain" description:"Transform the responses containing at least a single IP that matches specified addresses and CIDRs into NXDOMAIN. Can be specified multiple times."` - - // HostsFiles is the list of paths to the hosts files to resolve from. - HostsFiles []string `yaml:"hosts-files" long:"hosts-files" description:"List of paths to the hosts files relative to the root, can be specified multiple times"` - - // Timeout for outbound DNS queries to remote upstream servers in a - // human-readable form. Default is 10s. - Timeout decodableDuration `yaml:"timeout" long:"timeout" description:"Timeout for outbound DNS queries to remote upstream servers in a human-readable form" default:"10s"` - - // CacheMinTTL is the minimum TTL value for caching DNS entries, in seconds. - // It overrides the TTL value from the upstream server, if the one is less. - CacheMinTTL uint32 `yaml:"cache-min-ttl" long:"cache-min-ttl" description:"Minimum TTL value for DNS entries, in seconds. Capped at 3600. Artificially extending TTLs should only be done with careful consideration."` - - // CacheMaxTTL is the maximum TTL value for caching DNS entries, in seconds. - // It overrides the TTL value from the upstream server, if the one is - // greater. - CacheMaxTTL uint32 `yaml:"cache-max-ttl" long:"cache-max-ttl" description:"Maximum TTL value for DNS entries, in seconds."` - - // CacheSizeBytes is the cache size in bytes. Default is 64k. - CacheSizeBytes int `yaml:"cache-size" long:"cache-size" description:"Cache size (in bytes). Default: 64k"` - - // Ratelimit is the maximum number of requests per second. - Ratelimit int `yaml:"ratelimit" short:"r" long:"ratelimit" description:"Ratelimit (requests per second)"` - - // RatelimitSubnetLenIPv4 is a subnet length for IPv4 addresses used for - // rate limiting requests. - RatelimitSubnetLenIPv4 int `yaml:"ratelimit-subnet-len-ipv4" long:"ratelimit-subnet-len-ipv4" description:"Ratelimit subnet length for IPv4." default:"24"` - - // RatelimitSubnetLenIPv6 is a subnet length for IPv6 addresses used for - // rate limiting requests. - RatelimitSubnetLenIPv6 int `yaml:"ratelimit-subnet-len-ipv6" long:"ratelimit-subnet-len-ipv6" description:"Ratelimit subnet length for IPv6." default:"56"` - - // UDPBufferSize is the size of the UDP buffer in bytes. A value <= 0 will - // use the system default. - UDPBufferSize int `yaml:"udp-buf-size" long:"udp-buf-size" description:"Set the size of the UDP buffer in bytes. A value <= 0 will use the system default."` - - // MaxGoRoutines is the maximum number of goroutines. - MaxGoRoutines uint `yaml:"max-go-routines" long:"max-go-routines" description:"Set the maximum number of go routines. A zero value will not not set a maximum."` - - // TLSMinVersion is the minimum allowed version of TLS. - TLSMinVersion float32 `yaml:"tls-min-version" long:"tls-min-version" description:"Minimum TLS version, for example 1.0" optional:"yes"` - - // TLSMaxVersion is the maximum allowed version of TLS. - TLSMaxVersion float32 `yaml:"tls-max-version" long:"tls-max-version" description:"Maximum TLS version, for example 1.3" optional:"yes"` - - // Pprof defines whether the pprof information needs to be exposed via - // localhost:6060 or not. - Pprof bool `yaml:"pprof" long:"pprof" description:"If present, exposes pprof information on localhost:6060." optional:"yes" optional-value:"true"` - - // Version, if true, prints the program version, and exits. - Version bool `yaml:"version" long:"version" description:"Prints the program version"` - - // Verbose controls the verbosity of the output. - Verbose bool `yaml:"verbose" short:"v" long:"verbose" description:"Verbose output (optional)" optional:"yes" optional-value:"true"` - - // Insecure disables upstream servers TLS certificate verification. - Insecure bool `yaml:"insecure" long:"insecure" description:"Disable secure TLS certificate validation" optional:"yes" optional-value:"false"` - - // IPv6Disabled makes the server to respond with NODATA to all AAAA queries. - IPv6Disabled bool `yaml:"ipv6-disabled" long:"ipv6-disabled" description:"If specified, all AAAA requests will be replied with NoError RCode and empty answer" optional:"yes" optional-value:"true"` - - // HTTP3 controls whether HTTP/3 is enabled for this instance of dnsproxy. - // It enables HTTP/3 support for both the DoH upstreams and the DoH server. - HTTP3 bool `yaml:"http3" long:"http3" description:"Enable HTTP/3 support" optional:"yes" optional-value:"false"` - - // CacheOptimistic, if set to true, enables the optimistic DNS cache. That - // means that cached results will be served even if their cache TTL has - // already expired. - CacheOptimistic bool `yaml:"cache-optimistic" long:"cache-optimistic" description:"If specified, optimistic DNS cache is enabled" optional:"yes" optional-value:"true"` - - // Cache controls whether DNS responses are cached or not. - Cache bool `yaml:"cache" long:"cache" description:"If specified, DNS cache is enabled" optional:"yes" optional-value:"true"` - - // RefuseAny makes the server to refuse requests of type ANY. - RefuseAny bool `yaml:"refuse-any" long:"refuse-any" description:"If specified, refuse ANY requests" optional:"yes" optional-value:"true"` - - // EnableEDNSSubnet uses EDNS Client Subnet extension. - EnableEDNSSubnet bool `yaml:"edns" long:"edns" description:"Use EDNS Client Subnet extension" optional:"yes" optional-value:"true"` - - // DNS64 defines whether DNS64 functionality is enabled or not. - DNS64 bool `yaml:"dns64" long:"dns64" description:"If specified, dnsproxy will act as a DNS64 server" optional:"yes" optional-value:"true"` - - // UsePrivateRDNS makes the server to use private upstreams for reverse DNS - // lookups of private addresses, including the requests for authority - // records, such as SOA and NS. - UsePrivateRDNS bool `yaml:"use-private-rdns" long:"use-private-rdns" description:"If specified, use private upstreams for reverse DNS lookups of private addresses" optional:"yes" optional-value:"true"` -} - -// parseOptions returns options parsed from the command args or config file. -// If no options have been parsed returns a suitable exit code and an error. -func parseOptions() (opts *Options, exitCode int, err error) { - opts, hasHostsFlag, hostsEnabledByConf, exitCode, err := parseArgs() - if opts == nil { - // Don't wrap the error since it's informative enough as is. - return nil, exitCode, err - } - - parser := goFlags.NewParser(opts, goFlags.Default) - _, err = parser.Parse() - if err != nil { - if flagsErr, ok := err.(*goFlags.Error); ok && flagsErr.Type == goFlags.ErrHelp { - return nil, osutil.ExitCodeSuccess, nil - } - - return nil, osutil.ExitCodeArgumentError, nil - } - - if !hasHostsFlag { - opts.HostsFileEnabled = &decodableBool{value: hostsEnabledByConf} - } - - return opts, osutil.ExitCodeSuccess, nil -} - -// parseArgs returns options parsed from the config file. It returns nil opts -// if it meets [argVersion] flag or an error. hasHosts is true if the hosts are -// specified in the command line. hostsInConf is the value of the same option -// from the config file. -func parseArgs() (opts *Options, hasHosts, hostsInConf bool, exitCode int, err error) { - opts = &Options{} - - // TODO(e.burkov): Get rid of this crutch as the go-flags package is gone. - // See the issue in the TODO below. - hasHosts = false - hostsInConf = true - - // TODO(e.burkov, a.garipov): Use flag package and remove the manual - // options parsing. - // - // See https://github.com/AdguardTeam/dnsproxy/issues/182. - for _, arg := range os.Args { - if arg == argVersion { - fmt.Printf("dnsproxy version: %s\n", version.Version()) - - return nil, false, false, osutil.ExitCodeSuccess, nil - } else if strings.HasPrefix(arg, argConfigPath) { - confPath := strings.TrimPrefix(arg, argConfigPath) - fmt.Printf("dnsproxy config path: %s\n", confPath) - - err = parseConfigFile(opts, confPath) - if err != nil { - return nil, false, false, osutil.ExitCodeFailure, fmt.Errorf( - "parsing config file %s: %w", - confPath, - err, - ) - } - - if opts.HostsFileEnabled != nil { - hostsInConf = opts.HostsFileEnabled.value - } - } else { - hasHosts = hasHosts || strings.HasPrefix(arg, argHostsEnabled) - } - } - - return opts, hasHosts, hostsInConf, osutil.ExitCodeSuccess, nil -} - -// hostsFiles returns the list of hosts files to resolve from. It's empty if -// resolving from hosts files is disabled. -func (opts *Options) hostsFiles(ctx context.Context, l *slog.Logger) (paths []string, err error) { - if opts.HostsFileEnabled != nil && !opts.HostsFileEnabled.value { - l.DebugContext(ctx, "hosts files are disabled") - - return nil, nil - } - - l.DebugContext(ctx, "hosts files are enabled") - - if len(opts.HostsFiles) > 0 { - return opts.HostsFiles, nil - } - - paths, err = hostsfile.DefaultHostsPaths() - if err != nil { - return nil, fmt.Errorf("getting default hosts files: %w", err) - } - - l.DebugContext(ctx, "hosts files are not specified, using default", "paths", paths) - - return paths, nil -} diff --git a/internal/cmd/proxy.go b/internal/cmd/proxy.go new file mode 100644 index 000000000..e28a072d2 --- /dev/null +++ b/internal/cmd/proxy.go @@ -0,0 +1,528 @@ +package cmd + +import ( + "context" + "crypto/tls" + "fmt" + "log/slog" + "net" + "net/netip" + "net/url" + "os" + "strings" + "time" + + "github.com/AdguardTeam/dnsproxy/internal/dnsmsg" + "github.com/AdguardTeam/dnsproxy/internal/handler" + proxynetutil "github.com/AdguardTeam/dnsproxy/internal/netutil" + "github.com/AdguardTeam/dnsproxy/proxy" + "github.com/AdguardTeam/dnsproxy/upstream" + "github.com/AdguardTeam/golibs/errors" + "github.com/AdguardTeam/golibs/hostsfile" + "github.com/AdguardTeam/golibs/logutil/slogutil" + "github.com/AdguardTeam/golibs/netutil" + "github.com/AdguardTeam/golibs/osutil" + "github.com/ameshkov/dnscrypt/v2" + "gopkg.in/yaml.v3" +) + +// TODO(e.burkov): Use a separate type for the YAML configuration file. + +// createProxyConfig initializes [proxy.Config]. l must not be nil. +func createProxyConfig( + ctx context.Context, + l *slog.Logger, + conf *configuration, +) (proxyConf *proxy.Config, err error) { + hostsFiles, err := conf.hostsFiles(ctx, l) + if err != nil { + // Don't wrap the error since it's informative enough as is. + return nil, err + } + + reqHdlr, err := handler.NewDefault(&handler.DefaultConfig{ + Logger: l.With(slogutil.KeyPrefix, "default_handler"), + // TODO(e.burkov): Use the configured message constructor. + MessageConstructor: dnsmsg.DefaultMessageConstructor{}, + HaltIPv6: conf.IPv6Disabled, + HostsFiles: hostsFiles, + FileSystem: osutil.RootDirFS(), + }) + if err != nil { + return nil, fmt.Errorf("creating default handler: %w", err) + } + + proxyConf = &proxy.Config{ + Logger: l.With(slogutil.KeyPrefix, proxy.LogPrefix), + + RatelimitSubnetLenIPv4: conf.RatelimitSubnetLenIPv4, + RatelimitSubnetLenIPv6: conf.RatelimitSubnetLenIPv6, + + Ratelimit: conf.Ratelimit, + CacheEnabled: conf.Cache, + CacheSizeBytes: conf.CacheSizeBytes, + CacheMinTTL: conf.CacheMinTTL, + CacheMaxTTL: conf.CacheMaxTTL, + CacheOptimistic: conf.CacheOptimistic, + RefuseAny: conf.RefuseAny, + HTTP3: conf.HTTP3, + // TODO(e.burkov): The following CIDRs are aimed to match any address. + // This is not quite proper approach to be used by default so think + // about configuring it. + TrustedProxies: netutil.SliceSubnetSet{ + netip.MustParsePrefix("0.0.0.0/0"), + netip.MustParsePrefix("::0/0"), + }, + EnableEDNSClientSubnet: conf.EnableEDNSSubnet, + UDPBufferSize: conf.UDPBufferSize, + HTTPSServerName: conf.HTTPSServerName, + MaxGoroutines: conf.MaxGoRoutines, + UsePrivateRDNS: conf.UsePrivateRDNS, + PrivateSubnets: netutil.SubnetSetFunc(netutil.IsLocallyServed), + RequestHandler: reqHdlr.HandleRequest, + } + + if uiStr := conf.HTTPSUserinfo; uiStr != "" { + user, pass, ok := strings.Cut(uiStr, ":") + if ok { + proxyConf.Userinfo = url.UserPassword(user, pass) + } else { + proxyConf.Userinfo = url.User(user) + } + } + + conf.initBogusNXDomain(ctx, l, proxyConf) + + var errs []error + errs = append(errs, conf.initUpstreams(ctx, l, proxyConf)) + errs = append(errs, conf.initEDNS(ctx, l, proxyConf)) + errs = append(errs, conf.initTLSConfig(proxyConf)) + errs = append(errs, conf.initDNSCryptConfig(proxyConf)) + errs = append(errs, conf.initListenAddrs(proxyConf)) + errs = append(errs, conf.initSubnets(proxyConf)) + + return proxyConf, errors.Join(errs...) +} + +// isEmpty returns false if uc contains at least a single upstream. uc must not +// be nil. +// +// TODO(e.burkov): Think of a better way to validate the config. Perhaps, +// return an error from [ParseUpstreamsConfig] if no upstreams were initialized. +func isEmpty(uc *proxy.UpstreamConfig) (ok bool) { + return len(uc.Upstreams) == 0 && + len(uc.DomainReservedUpstreams) == 0 && + len(uc.SpecifiedDomainUpstreams) == 0 +} + +// defaultLocalTimeout is the default timeout for local operations. +const defaultLocalTimeout = 1 * time.Second + +// initUpstreams inits upstream-related config fields. +// +// TODO(d.kolyshev): Join errors. +func (conf *configuration) initUpstreams( + ctx context.Context, + l *slog.Logger, + config *proxy.Config, +) (err error) { + httpVersions := upstream.DefaultHTTPVersions + if conf.HTTP3 { + httpVersions = []upstream.HTTPVersion{ + upstream.HTTPVersion3, + upstream.HTTPVersion2, + upstream.HTTPVersion11, + } + } + + timeout := conf.Timeout.Duration + bootOpts := &upstream.Options{ + Logger: l, + HTTPVersions: httpVersions, + InsecureSkipVerify: conf.Insecure, + Timeout: timeout, + } + boot, err := initBootstrap(ctx, l, conf.BootstrapDNS, bootOpts) + if err != nil { + return fmt.Errorf("initializing bootstrap: %w", err) + } + + upsOpts := &upstream.Options{ + Logger: l, + HTTPVersions: httpVersions, + InsecureSkipVerify: conf.Insecure, + Bootstrap: boot, + Timeout: timeout, + } + upstreams := loadServersList(conf.Upstreams) + + config.UpstreamConfig, err = proxy.ParseUpstreamsConfig(upstreams, upsOpts) + if err != nil { + return fmt.Errorf("parsing upstreams configuration: %w", err) + } + + privateUpsOpts := &upstream.Options{ + Logger: l, + HTTPVersions: httpVersions, + Bootstrap: boot, + Timeout: min(defaultLocalTimeout, timeout), + } + privateUpstreams := loadServersList(conf.PrivateRDNSUpstreams) + + private, err := proxy.ParseUpstreamsConfig(privateUpstreams, privateUpsOpts) + if err != nil { + return fmt.Errorf("parsing private rdns upstreams configuration: %w", err) + } + + if !isEmpty(private) { + config.PrivateRDNSUpstreamConfig = private + } + + fallbackUpstreams := loadServersList(conf.Fallbacks) + fallbacks, err := proxy.ParseUpstreamsConfig(fallbackUpstreams, upsOpts) + if err != nil { + return fmt.Errorf("parsing fallback upstreams configuration: %w", err) + } + + if !isEmpty(fallbacks) { + config.Fallbacks = fallbacks + } + + if conf.UpstreamMode != "" { + err = config.UpstreamMode.UnmarshalText([]byte(conf.UpstreamMode)) + if err != nil { + return fmt.Errorf("parsing upstream mode: %w", err) + } + + return nil + } + + config.UpstreamMode = proxy.UpstreamModeLoadBalance + + return nil +} + +// initBootstrap initializes the [upstream.Resolver] for bootstrapping upstream +// servers. It returns the default resolver if no bootstraps were specified. +// The returned resolver will also use system hosts files first. +func initBootstrap( + ctx context.Context, + l *slog.Logger, + bootstraps []string, + opts *upstream.Options, +) (r upstream.Resolver, err error) { + var resolvers []upstream.Resolver + + for i, b := range bootstraps { + var ur *upstream.UpstreamResolver + ur, err = upstream.NewUpstreamResolver(b, opts) + if err != nil { + return nil, fmt.Errorf("creating bootstrap resolver at index %d: %w", i, err) + } + + resolvers = append(resolvers, upstream.NewCachingResolver(ur)) + } + + switch len(resolvers) { + case 0: + etcHosts, hostsErr := upstream.NewDefaultHostsResolver(osutil.RootDirFS(), l) + if hostsErr != nil { + l.ErrorContext(ctx, "creating default hosts resolver", slogutil.KeyError, hostsErr) + + return net.DefaultResolver, nil + } + + return upstream.ConsequentResolver{etcHosts, net.DefaultResolver}, nil + case 1: + return resolvers[0], nil + default: + return upstream.ParallelResolver(resolvers), nil + } +} + +// initEDNS inits EDNS-related config fields. +func (conf *configuration) initEDNS( + ctx context.Context, + l *slog.Logger, + config *proxy.Config, +) (err error) { + if conf.EDNSAddr == "" { + return nil + } + + if !conf.EnableEDNSSubnet { + l.WarnContext(ctx, "--edns is required", "--edns-addr", conf.EDNSAddr) + + return nil + } + + config.EDNSAddr, err = netutil.ParseIP(conf.EDNSAddr) + if err != nil { + return fmt.Errorf("parsing edns-addr: %w", err) + } + + return nil +} + +// initBogusNXDomain inits BogusNXDomain structure. +func (conf *configuration) initBogusNXDomain(ctx context.Context, l *slog.Logger, config *proxy.Config) { + if len(conf.BogusNXDomain) == 0 { + return + } + + for i, s := range conf.BogusNXDomain { + p, err := proxynetutil.ParseSubnet(s) + if err != nil { + // TODO(a.garipov): Consider returning this err as a proper error. + l.WarnContext(ctx, "parsing bogus nxdomain", "index", i, slogutil.KeyError, err) + } else { + config.BogusNXDomain = append(config.BogusNXDomain, p) + } + } +} + +// initTLSConfig inits the TLS config. +func (conf *configuration) initTLSConfig(config *proxy.Config) (err error) { + if conf.TLSCertPath != "" && conf.TLSKeyPath != "" { + var tlsConfig *tls.Config + tlsConfig, err = newTLSConfig(conf) + if err != nil { + return fmt.Errorf("loading TLS config: %w", err) + } + + config.TLSConfig = tlsConfig + } + + return nil +} + +// initDNSCryptConfig inits the DNSCrypt config. +func (conf *configuration) initDNSCryptConfig(config *proxy.Config) (err error) { + if conf.DNSCryptConfigPath == "" { + return + } + + b, err := os.ReadFile(conf.DNSCryptConfigPath) + if err != nil { + return fmt.Errorf("reading DNSCrypt config %q: %w", conf.DNSCryptConfigPath, err) + } + + rc := &dnscrypt.ResolverConfig{} + err = yaml.Unmarshal(b, rc) + if err != nil { + return fmt.Errorf("unmarshalling DNSCrypt config: %w", err) + } + + cert, err := rc.CreateCert() + if err != nil { + return fmt.Errorf("creating DNSCrypt certificate: %w", err) + } + + config.DNSCryptResolverCert = cert + config.DNSCryptProviderName = rc.ProviderName + + return nil +} + +// parseListenAddrs returns a slice of listen IP addresses from the given +// options. In case no addresses are specified by options returns a slice with +// the IPv4 unspecified address "0.0.0.0". +// +// TODO(d.kolyshev): Join errors. +func parseListenAddrs(addrStrs []string) (addrs []netip.Addr, err error) { + for i, a := range addrStrs { + var ip netip.Addr + ip, err = netip.ParseAddr(a) + if err != nil { + return addrs, fmt.Errorf("parsing listen address at index %d: %s", i, a) + } + + addrs = append(addrs, ip) + } + + if len(addrs) == 0 { + // If ListenAddrs has not been parsed through config file nor command + // line we set it to "0.0.0.0". + // + // TODO(a.garipov): Consider using localhost. + addrs = append(addrs, netip.IPv4Unspecified()) + } + + return addrs, nil +} + +// initListenAddrs sets up proxy configuration listen IP addresses. +func (conf *configuration) initListenAddrs(config *proxy.Config) (err error) { + addrs, err := parseListenAddrs(conf.ListenAddrs) + if err != nil { + return fmt.Errorf("parsing listen addresses: %w", err) + } + + if len(conf.ListenPorts) == 0 { + // If ListenPorts has not been parsed through config file nor command + // line we set it to 53. + conf.ListenPorts = []int{53} + } + + for _, port := range conf.ListenPorts { + for _, ip := range addrs { + addrPort := netip.AddrPortFrom(ip, uint16(port)) + + config.UDPListenAddr = append(config.UDPListenAddr, net.UDPAddrFromAddrPort(addrPort)) + config.TCPListenAddr = append(config.TCPListenAddr, net.TCPAddrFromAddrPort(addrPort)) + } + } + + initTLSListenAddrs(config, conf, addrs) + initDNSCryptListenAddrs(config, conf, addrs) + + return nil +} + +// initTLSListenAddrs sets up proxy configuration TLS listen addresses. +func initTLSListenAddrs(proxyConf *proxy.Config, conf *configuration, addrs []netip.Addr) { + if proxyConf.TLSConfig == nil { + return + } + + for _, ip := range addrs { + for _, port := range conf.TLSListenPorts { + a := net.TCPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16(port))) + proxyConf.TLSListenAddr = append(proxyConf.TLSListenAddr, a) + } + + for _, port := range conf.HTTPSListenPorts { + a := net.TCPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16(port))) + proxyConf.HTTPSListenAddr = append(proxyConf.HTTPSListenAddr, a) + } + + for _, port := range conf.QUICListenPorts { + a := net.UDPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16(port))) + proxyConf.QUICListenAddr = append(proxyConf.QUICListenAddr, a) + } + } +} + +// initDNSCryptListenAddrs sets up proxy configuration DNSCrypt listen +// addresses. +func initDNSCryptListenAddrs(proxyConf *proxy.Config, conf *configuration, addrs []netip.Addr) { + if proxyConf.DNSCryptResolverCert == nil || proxyConf.DNSCryptProviderName == "" { + return + } + + for _, port := range conf.DNSCryptListenPorts { + p := uint16(port) + + for _, ip := range addrs { + addrPort := netip.AddrPortFrom(ip, p) + + tcp := net.TCPAddrFromAddrPort(addrPort) + proxyConf.DNSCryptTCPListenAddr = append(proxyConf.DNSCryptTCPListenAddr, tcp) + + udp := net.UDPAddrFromAddrPort(addrPort) + proxyConf.DNSCryptUDPListenAddr = append(proxyConf.DNSCryptUDPListenAddr, udp) + } + } +} + +// initSubnets sets the DNS64 configuration into conf. +// +// TODO(d.kolyshev): Join errors. +func (conf *configuration) initSubnets(proxyConf *proxy.Config) (err error) { + if proxyConf.UseDNS64 = conf.DNS64; proxyConf.UseDNS64 { + for i, p := range conf.DNS64Prefix { + var pref netip.Prefix + pref, err = netip.ParsePrefix(p) + if err != nil { + return fmt.Errorf("parsing dns64 prefix at index %d: %w", i, err) + } + + proxyConf.DNS64Prefs = append(proxyConf.DNS64Prefs, pref) + } + } + + if !conf.UsePrivateRDNS { + return nil + } + + return conf.initPrivateSubnets(proxyConf) +} + +// initSubnets sets the private subnets configuration into conf. +func (conf *configuration) initPrivateSubnets(proxyConf *proxy.Config) (err error) { + private := make([]netip.Prefix, 0, len(conf.PrivateSubnets)) + for i, p := range conf.PrivateSubnets { + var pref netip.Prefix + pref, err = netip.ParsePrefix(p) + if err != nil { + return fmt.Errorf("parsing private subnet at index %d: %w", i, err) + } + + private = append(private, pref) + } + + if len(private) > 0 { + proxyConf.PrivateSubnets = netutil.SliceSubnetSet(private) + } + + return nil +} + +// loadServersList loads a list of DNS servers from the specified list. The +// thing is that the user may specify either a server address or the path to a +// file with a list of addresses. This method takes care of it, it reads the +// file and loads servers from this file if needed. +func loadServersList(sources []string) []string { + var servers []string + + for _, source := range sources { + // #nosec G304 -- Trust the file path that is given in the + // configuration. + data, err := os.ReadFile(source) + if err != nil { + // Ignore errors, just consider it a server address and not a file. + servers = append(servers, source) + } + + lines := strings.Split(string(data), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + + // Ignore comments in the file. + if line == "" || + strings.HasPrefix(line, "!") || + strings.HasPrefix(line, "#") { + continue + } + + servers = append(servers, line) + } + } + + return servers +} + +// hostsFiles returns the list of hosts files to resolve from. It's empty if +// resolving from hosts files is disabled. +func (conf *configuration) hostsFiles(ctx context.Context, l *slog.Logger) (paths []string, err error) { + if !conf.HostsFileEnabled { + l.DebugContext(ctx, "hosts files are disabled") + + return nil, nil + } + + l.DebugContext(ctx, "hosts files are enabled") + + if len(conf.HostsFiles) > 0 { + return conf.HostsFiles, nil + } + + paths, err = hostsfile.DefaultHostsPaths() + if err != nil { + return nil, fmt.Errorf("getting default hosts files: %w", err) + } + + l.DebugContext(ctx, "hosts files are not specified, using default", "paths", paths) + + return paths, nil +} diff --git a/internal/cmd/tls.go b/internal/cmd/tls.go index f2b8ad406..c001d1206 100644 --- a/internal/cmd/tls.go +++ b/internal/cmd/tls.go @@ -9,12 +9,12 @@ import ( // NewTLSConfig returns the TLS config that includes a certificate. Use it for // server TLS configuration or for a client certificate. If caPath is empty, // system CAs will be used. -func newTLSConfig(options *Options) (c *tls.Config, err error) { +func newTLSConfig(conf *configuration) (c *tls.Config, err error) { // Set default TLS min/max versions tlsMinVersion := tls.VersionTLS10 tlsMaxVersion := tls.VersionTLS13 - switch options.TLSMinVersion { + switch conf.TLSMinVersion { case 1.1: tlsMinVersion = tls.VersionTLS11 case 1.2: @@ -23,7 +23,7 @@ func newTLSConfig(options *Options) (c *tls.Config, err error) { tlsMinVersion = tls.VersionTLS13 } - switch options.TLSMaxVersion { + switch conf.TLSMaxVersion { case 1.0: tlsMaxVersion = tls.VersionTLS10 case 1.1: @@ -32,7 +32,7 @@ func newTLSConfig(options *Options) (c *tls.Config, err error) { tlsMaxVersion = tls.VersionTLS12 } - cert, err := loadX509KeyPair(options.TLSCertPath, options.TLSKeyPath) + cert, err := loadX509KeyPair(conf.TLSCertPath, conf.TLSKeyPath) if err != nil { return nil, fmt.Errorf("loading TLS cert: %s", err) }