diff --git a/go.mod b/go.mod index b71655e2..1a648e1d 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 dce68d90..c8343562 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 00000000..5afbef0e --- /dev/null +++ b/internal/cmd/args.go @@ -0,0 +1,581 @@ +package cmd + +import ( + "encoding" + "flag" + "fmt" + "io" + "os" + "slices" + "strings" + + "github.com/AdguardTeam/dnsproxy/internal/version" + "github.com/AdguardTeam/golibs/osutil" +) + +// 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 + 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, the description, and the +// default value. +type commandLineOption struct { + defaultValue any + description string + long string + short string + valueType string +} + +// commandLineOptions are all command-line options currently supported by the +// binary. +// +// TODO(d.kolyshev): !! Handle required and optionals. +// TODO(d.kolyshev): !! Check multiple times specified opts. +var commandLineOptions = []*commandLineOption{ + configPathIdx: { + defaultValue: "", + 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: { + defaultValue: "stdout", + description: `Path to the log file.`, + long: "output", + short: "o", + valueType: "path", + }, + tlsCertPathIdx: { + defaultValue: "", + description: "Path to a file with the certificate chain.", + long: "tls-crt", + short: "c", + valueType: "path", + }, + tlsKeyPathIdx: { + defaultValue: "", + description: "Path to a file with the private key.", + long: "tls-key", + short: "k", + valueType: "path", + }, + httpsServerNameIdx: { + defaultValue: "dnsproxy", + description: "Set the Server header for the responses from the HTTPS server.", + long: "https-server-name", + short: "", + valueType: "name", + }, + httpsUserinfoIdx: { + defaultValue: "", + description: "If set, all DoH queries are required to have this basic authentication information.", + long: "https-userinfo", + short: "", + valueType: "name", + }, + dnsCryptConfigPathIdx: { + defaultValue: "", + 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: { + defaultValue: "", + description: "Send EDNS Client Address.", + long: "edns-addr", + short: "", + valueType: "", + }, + upstreamModeIdx: { + defaultValue: "load_balance", + description: "Defines the upstreams logic mode, possible values: load_balance, parallel, fastest_addr (default: load_balance).", + long: "upstream-mode", + short: "", + valueType: "", + }, + listenAddrsIdx: { + defaultValue: "", + description: "Listening addresses.", + long: "listen", + short: "l", + valueType: "", + }, + listenPortsIdx: { + defaultValue: "", + description: "Listening ports. Zero value disables TCP and UDP listeners.", + long: "port", + short: "p", + valueType: "", + }, + httpsListenPortsIdx: { + defaultValue: "", + description: "Listening ports for DNS-over-HTTPS.", + long: "https-port", + short: "s", + valueType: "", + }, + tlsListenPortsIdx: { + defaultValue: "", + description: "Listening ports for DNS-over-TLS.", + long: "tls-port", + short: "t", + valueType: "", + }, + quicListenPortsIdx: { + defaultValue: "", + description: "Listening ports for DNS-over-QUIC.", + long: "quic-port", + short: "q", + valueType: "", + }, + dnsCryptListenPortsIdx: { + defaultValue: "", + description: "Listening ports for DNSCrypt.", + long: "dnscrypt-port", + short: "y", + valueType: "", + }, + upstreamsIdx: { + defaultValue: "", + 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: { + defaultValue: "", + description: "Bootstrap DNS for DoH and DoT, can be specified multiple times (default: use system-provided).", + long: "bootstrap", + short: "b", + valueType: "", + }, + fallbacksIdx: { + defaultValue: "", + 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: { + defaultValue: "", + 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: { + defaultValue: "", + 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: "", + }, + privateSubnetsIdx: { + defaultValue: "", + description: "Private subnets to use for reverse DNS lookups of private addresses.", + long: "private-subnets", + short: "", + valueType: "", + }, + bogusNXDomainIdx: { + defaultValue: "", + 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: "", + }, + hostsFilesIdx: { + defaultValue: "", + description: "List of paths to the hosts files relative to the root, can be specified multiple times.", + long: "hosts-files", + short: "", + valueType: "", + }, + timeoutIdx: { + defaultValue: "10s", + description: "Timeout for outbound DNS queries to remote upstream servers in a human-readable form", + long: "timeout", + short: "", + valueType: "", + }, + cacheMinTTLIdx: { + defaultValue: "", + 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: "", + }, + cacheMaxTTLIdx: { + defaultValue: "", + description: "Maximum TTL value for DNS entries, in seconds.", + long: "cache-max-ttl", + short: "", + valueType: "", + }, + // TODO(d.kolyshev): Set default. + cacheSizeBytesIdx: { + defaultValue: "", + description: "Cache size (in bytes). Default: 64k.", + long: "cache-size", + short: "", + valueType: "", + }, + ratelimitIdx: { + defaultValue: "", + description: "Ratelimit (requests per second).", + long: "ratelimit", + short: "r", + valueType: "", + }, + ratelimitSubnetLenIPv4Idx: { + defaultValue: 24, + description: "Ratelimit subnet length for IPv4.", + long: "ratelimit-subnet-len-ipv4", + short: "", + valueType: "", + }, + ratelimitSubnetLenIPv6Idx: { + defaultValue: 56, + description: "Ratelimit subnet length for IPv6.", + long: "ratelimit-subnet-len-ipv6", + short: "", + valueType: "", + }, + udpBufferSizeIdx: { + defaultValue: "", + description: "Set the size of the UDP buffer in bytes. A value <= 0 will use the system default.", + long: "udp-buf-size", + short: "", + valueType: "", + }, + maxGoRoutinesIdx: { + defaultValue: "", + description: "Set the maximum number of go routines. A zero value will not not set a maximum.", + long: "max-go-routines", + short: "", + valueType: "", + }, + tlsMinVersionIdx: { + defaultValue: "", + description: "Minimum TLS version, for example 1.0.", + long: "tls-min-version", + short: "", + valueType: "", + }, + tlsMaxVersionIdx: { + defaultValue: "", + description: "Maximum TLS version, for example 1.3.", + long: "tls-max-version", + short: "", + valueType: "", + }, + hostsFileEnabledIdx: { + defaultValue: "", + description: "If specified, use hosts files for resolving.", + long: "hosts-file-enabled", + short: "", + valueType: "", + }, + pprofIdx: { + defaultValue: "", + description: "If present, exposes pprof information on localhost:6060.", + long: "pprof", + short: "", + valueType: "", + }, + versionIdx: { + defaultValue: "", + description: "Prints the program version.", + long: "version", + short: "", + valueType: "", + }, + verboseIdx: { + defaultValue: "", + description: "Verbose output.", + long: "verbose", + short: "v", + valueType: "", + }, + insecureIdx: { + defaultValue: "", + description: "Disable secure TLS certificate validation.", + long: "insecure", + short: "", + valueType: "", + }, + ipv6DisabledIdx: { + defaultValue: "", + description: "If specified, all AAAA requests will be replied with NoError RCode and empty answer.", + long: "ipv6-disabled", + short: "", + valueType: "", + }, + http3Idx: { + defaultValue: "", + description: "Enable HTTP/3 support.", + long: "http3", + short: "", + valueType: "", + }, + cacheOptimisticIdx: { + defaultValue: "", + description: "If specified, optimistic DNS cache is enabled.", + long: "cache-optimistic", + short: "", + valueType: "", + }, + cacheIdx: { + defaultValue: "", + description: "If specified, DNS cache is enabled.", + long: "cache", + short: "", + valueType: "", + }, + refuseAnyIdx: { + defaultValue: "", + description: "If specified, refuses ANY requests.", + long: "refuse-any", + short: "", + valueType: "", + }, + enableEDNSSubnetIdx: { + defaultValue: "", + description: "Use EDNS Client Subnet extension.", + long: "edns", + short: "", + valueType: "", + }, + dns64Idx: { + defaultValue: "", + description: "If specified, dnsproxy will act as a DNS64 server.", + long: "dns64", + short: "", + valueType: "", + }, + usePrivateRDNSIdx: { + defaultValue: "", + 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. +func parseCmdLineOptions(cmdName string, args []string) (opts *Options, err error) { + flags := flag.NewFlagSet(cmdName, flag.ContinueOnError) + + opts = &Options{} + for i, fieldPtr := range []any{ + configPathIdx: &opts.ConfigPath, + logOutputIdx: &opts.LogOutput, + tlsCertPathIdx: &opts.TLSCertPath, + tlsKeyPathIdx: &opts.TLSKeyPath, + httpsServerNameIdx: &opts.HTTPSServerName, + httpsUserinfoIdx: &opts.HTTPSUserinfo, + dnsCryptConfigPathIdx: &opts.DNSCryptConfigPath, + ednsAddrIdx: &opts.EDNSAddr, + upstreamModeIdx: &opts.UpstreamMode, + listenAddrsIdx: &opts.ListenAddrs, + listenPortsIdx: &opts.ListenPorts, + httpsListenPortsIdx: &opts.HTTPSListenPorts, + tlsListenPortsIdx: &opts.TLSListenPorts, + quicListenPortsIdx: &opts.QUICListenPorts, + dnsCryptListenPortsIdx: &opts.DNSCryptListenPorts, + upstreamsIdx: &opts.Upstreams, + bootstrapDNSIdx: &opts.BootstrapDNS, + fallbacksIdx: &opts.Fallbacks, + privateRDNSUpstreamsIdx: &opts.PrivateRDNSUpstreams, + dns64PrefixIdx: &opts.DNS64Prefix, + privateSubnetsIdx: &opts.PrivateSubnets, + bogusNXDomainIdx: &opts.BogusNXDomain, + hostsFilesIdx: &opts.HostsFiles, + timeoutIdx: &opts.Timeout, + cacheMinTTLIdx: &opts.CacheMinTTL, + cacheMaxTTLIdx: &opts.CacheMaxTTL, + cacheSizeBytesIdx: &opts.CacheSizeBytes, + ratelimitIdx: &opts.Ratelimit, + ratelimitSubnetLenIPv4Idx: &opts.RatelimitSubnetLenIPv4, + ratelimitSubnetLenIPv6Idx: &opts.RatelimitSubnetLenIPv6, + udpBufferSizeIdx: &opts.UDPBufferSize, + maxGoRoutinesIdx: &opts.MaxGoRoutines, + tlsMinVersionIdx: &opts.TLSMinVersion, + tlsMaxVersionIdx: &opts.TLSMaxVersion, + hostsFileEnabledIdx: &opts.HostsFileEnabled, + pprofIdx: &opts.Pprof, + versionIdx: &opts.Version, + verboseIdx: &opts.Verbose, + insecureIdx: &opts.Insecure, + ipv6DisabledIdx: &opts.IPv6Disabled, + http3Idx: &opts.HTTP3, + cacheOptimisticIdx: &opts.CacheOptimistic, + cacheIdx: &opts.Cache, + refuseAnyIdx: &opts.RefuseAny, + enableEDNSSubnetIdx: &opts.EnableEDNSSubnet, + dns64Idx: &opts.DNS64, + usePrivateRDNSIdx: &opts.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 nil, err + } + + return opts, nil +} + +// 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: + flags.StringVar(fieldPtr, o.long, o.defaultValue.(string), o.description) + if o.short != "" { + flags.StringVar(fieldPtr, o.short, o.defaultValue.(string), o.description) + } + case *bool: + flags.BoolVar(fieldPtr, o.long, o.defaultValue.(bool), o.description) + if o.short != "" { + flags.BoolVar(fieldPtr, o.short, o.defaultValue.(bool), o.description) + } + case encoding.TextUnmarshaler: + flags.TextVar(fieldPtr, o.long, o.defaultValue.(encoding.TextMarshaler), o.description) + if o.short != "" { + flags.TextVar(fieldPtr, o.short, o.defaultValue.(encoding.TextMarshaler), o.description) + } + default: + panic(fmt.Errorf("unexpected field pointer type %T", fieldPtr)) + } +} + +// 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. + if shouldIncludeDefault(o.defaultValue) { + _, _ = fmt.Fprintf(b, " \t%s (Default value: %q)\n", o.description, o.defaultValue) + } else { + _, _ = fmt.Fprintf(b, " \t%s\n", o.description) + } + } + + _, _ = io.WriteString(output, b.String()) +} + +// shouldIncludeDefault returns true if this default value should be printed. +func shouldIncludeDefault(v any) (ok bool) { + switch v := v.(type) { + case bool: + return v + case string: + return v != "" + default: + return v == nil + } +} + +// 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(opts *Options, parseErr error) (exitCode int, needExit bool) { + if parseErr != nil { + // Assume that usage has already been printed. + return osutil.ExitCodeArgumentError, true + } + + if opts.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 957d3965..92187cc9 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -22,17 +22,14 @@ 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() - if err != nil { - _, _ = fmt.Fprintln(os.Stderr, err) - } - - if opts == nil { + opts, exitCode, needExit := parseOptions() + if needExit { os.Exit(exitCode) } logOutput := os.Stdout if opts.LogOutput != "" { + var err error // #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) @@ -59,7 +56,7 @@ func Main() { runPprof(l) } - err = runProxy(ctx, l, opts) + err := runProxy(ctx, l, opts) if err != nil { l.ErrorContext(ctx, "running dnsproxy", slogutil.KeyError, err) diff --git a/internal/cmd/config.go b/internal/cmd/config.go index cefdb2a7..c4a54eac 100644 --- a/internal/cmd/config.go +++ b/internal/cmd/config.go @@ -10,6 +10,7 @@ import ( "net/url" "os" "strings" + "time" "github.com/AdguardTeam/dnsproxy/internal/dnsmsg" "github.com/AdguardTeam/dnsproxy/internal/handler" @@ -17,6 +18,7 @@ import ( "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" @@ -26,23 +28,6 @@ import ( // TODO(e.burkov): Use a separate type for the YAML configuration file. -// 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) - } - - err = yaml.Unmarshal(b, options) - if err != nil { - return fmt.Errorf("unmarshalling file: %w", err) - } - - return nil -} - // createProxyConfig initializes [proxy.Config]. l must not be nil. func createProxyConfig( ctx context.Context, @@ -130,6 +115,9 @@ func isEmpty(uc *proxy.UpstreamConfig) (ok bool) { 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. @@ -152,7 +140,7 @@ func (opts *Options) initUpstreams( Logger: l, HTTPVersions: httpVersions, InsecureSkipVerify: opts.Insecure, - Timeout: timeout.Duration, + Timeout: timeout, } boot, err := initBootstrap(ctx, l, opts.BootstrapDNS, bootOpts) if err != nil { @@ -164,7 +152,7 @@ func (opts *Options) initUpstreams( HTTPVersions: httpVersions, InsecureSkipVerify: opts.Insecure, Bootstrap: boot, - Timeout: timeout.Duration, + Timeout: timeout, } upstreams := loadServersList(opts.Upstreams) @@ -177,7 +165,7 @@ func (opts *Options) initUpstreams( Logger: l, HTTPVersions: httpVersions, Bootstrap: boot, - Timeout: min(defaultLocalTimeout, timeout.Duration), + Timeout: min(defaultLocalTimeout, timeout), } privateUpstreams := loadServersList(opts.PrivateRDNSUpstreams) @@ -513,3 +501,28 @@ func loadServersList(sources []string) []string { return servers } + +// 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 { + 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/options.go b/internal/cmd/options.go index acd83711..541ffe89 100644 --- a/internal/cmd/options.go +++ b/internal/cmd/options.go @@ -1,346 +1,230 @@ 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" + "gopkg.in/yaml.v3" ) -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. +// Options represents dnsproxy configuration. 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:""` + // ConfigPath is the path to the configuration file. + ConfigPath string // 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."` + LogOutput string `yaml:"output"` // 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"` + TLSCertPath string `yaml:"tls-crt"` // 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"` + TLSKeyPath string `yaml:"tls-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"` + HTTPSServerName string `yaml:"https-server-name"` // 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."` + HTTPSUserinfo string `yaml:"https-userinfo"` // 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"` + DNSCryptConfigPath string `yaml:"dnscrypt-config"` // EDNSAddr is the custom EDNS Client Address to send. - EDNSAddr string `yaml:"edns-addr" long:"edns-addr" description:"Send EDNS Client Address"` + EDNSAddr string `yaml:"edns-addr"` // 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"` + UpstreamMode string `yaml:"upstream-mode"` // ListenAddrs is the list of server's listen addresses. - ListenAddrs []string `yaml:"listen-addrs" short:"l" long:"listen" description:"Listening addresses"` + ListenAddrs []string `yaml:"listen-addrs"` // 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"` + ListenPorts []int `yaml:"listen-ports"` // 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"` + HTTPSListenPorts []int `yaml:"https-port"` // 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"` + TLSListenPorts []int `yaml:"tls-port"` // 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"` + QUICListenPorts []int `yaml:"quic-port"` // DNSCryptListenPorts are the ports server listens on for DNSCrypt. - DNSCryptListenPorts []int `yaml:"dnscrypt-port" short:"y" long:"dnscrypt-port" description:"Listening ports for DNSCrypt"` + DNSCryptListenPorts []int `yaml:"dnscrypt-port"` // 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"` + Upstreams []string `yaml:"upstream"` // 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)"` + BootstrapDNS []string `yaml:"bootstrap"` // 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"` + Fallbacks []string `yaml:"fallback"` // 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"` + PrivateRDNSUpstreams []string `yaml:"private-rdns-upstream"` // 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"` + DNS64Prefix []string `yaml:"dns64-prefix"` // 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"` + PrivateSubnets []string `yaml:"private-subnets"` // 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."` + BogusNXDomain []string `yaml:"bogus-nxdomain"` // 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"` + HostsFiles []string `yaml:"hosts-files"` // 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"` + Timeout timeutil.Duration `yaml:"timeout"` // 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."` + CacheMinTTL uint32 `yaml:"cache-min-ttl"` // 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."` + CacheMaxTTL uint32 `yaml:"cache-max-ttl"` // 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"` + CacheSizeBytes int `yaml:"cache-size"` // Ratelimit is the maximum number of requests per second. - Ratelimit int `yaml:"ratelimit" short:"r" long:"ratelimit" description:"Ratelimit (requests per second)"` + Ratelimit int `yaml:"ratelimit"` // 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"` + RatelimitSubnetLenIPv4 int `yaml:"ratelimit-subnet-len-ipv4"` // 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"` + RatelimitSubnetLenIPv6 int `yaml:"ratelimit-subnet-len-ipv6"` // 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."` + UDPBufferSize int `yaml:"udp-buf-size"` // 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."` + MaxGoRoutines uint `yaml:"max-go-routines"` // 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"` + TLSMinVersion float32 `yaml:"tls-min-version"` // 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"` + TLSMaxVersion float32 `yaml:"tls-max-version"` + + // HostsFileEnabled controls whether hosts files are used for resolving or + // not. + HostsFileEnabled bool `yaml:"hosts-file-enabled"` // 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"` + Pprof bool `yaml:"pprof"` // Version, if true, prints the program version, and exits. - Version bool `yaml:"version" long:"version" description:"Prints the program version"` + Version bool `yaml:"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"` + Verbose bool `yaml:"verbose"` // Insecure disables upstream servers TLS certificate verification. - Insecure bool `yaml:"insecure" long:"insecure" description:"Disable secure TLS certificate validation" optional:"yes" optional-value:"false"` + Insecure bool `yaml:"insecure"` // 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"` + IPv6Disabled bool `yaml:"ipv6-disabled"` // 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"` + HTTP3 bool `yaml:"http3"` // 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"` + CacheOptimistic bool `yaml:"cache-optimistic"` // 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"` + Cache bool `yaml:"cache"` // 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"` + RefuseAny bool `yaml:"refuse-any"` // EnableEDNSSubnet uses EDNS Client Subnet extension. - EnableEDNSSubnet bool `yaml:"edns" long:"edns" description:"Use EDNS Client Subnet extension" optional:"yes" optional-value:"true"` + EnableEDNSSubnet bool `yaml:"edns"` // 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"` + DNS64 bool `yaml:"dns64"` // 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"` + UsePrivateRDNS bool `yaml:"use-private-rdns"` } -// 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} +// parseOptions returns options parsed from the command args or config file. If +// no options have been parsed, it returns a suitable exit code. +func parseOptions() (opts *Options, exitCode int, needExit bool) { + opts, err := parseCmdLineOptions(os.Args[0], os.Args[1:]) + exitCode, needExit = processCmdLineOptions(opts, err) + if needExit { + return nil, exitCode, true } - return opts, osutil.ExitCodeSuccess, nil -} + confPath := opts.ConfigPath + if confPath != "" { + fmt.Printf("dnsproxy config path: %s\n", confPath) -// 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{} + hostsFlagEnabled := opts.HostsFileEnabled - // 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 + err = parseConfigFile(opts, confPath) + if err != nil { + _, _ = fmt.Fprintln(os.Stderr, fmt.Errorf( + "parsing config file %s: %w", + confPath, + err, + )) - // 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 nil, osutil.ExitCodeFailure, true } - } - - 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 + // Command-line flag has priority. + opts.HostsFileEnabled = hostsFlagEnabled || opts.HostsFileEnabled } - l.DebugContext(ctx, "hosts files are enabled") + return opts, exitCode, false +} - if len(opts.HostsFiles) > 0 { - return opts.HostsFiles, nil +// 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) } - paths, err = hostsfile.DefaultHostsPaths() + err = yaml.Unmarshal(b, options) if err != nil { - return nil, fmt.Errorf("getting default hosts files: %w", err) + return fmt.Errorf("unmarshalling file: %w", err) } - l.DebugContext(ctx, "hosts files are not specified, using default", "paths", paths) - - return paths, nil + return nil }