├── embed ├── bypassList ├── specList ├── tldnList └── VERSION ├── internal ├── README.md ├── cmd │ ├── const.go │ ├── tls.go │ ├── flag.go │ ├── cmd.go │ ├── config.go │ ├── proxy.go │ └── args.go ├── netutil │ ├── paths.go │ ├── paths_windows.go │ ├── paths_unix.go │ └── netutil.go ├── handler │ ├── ipv6halt.go │ ├── default.go │ ├── hosts.go │ └── constructor.go └── dnsmsg │ └── constructor.go ├── .gitignore ├── update-list.sh ├── Dockerfile ├── LICENSE ├── Makefile ├── go.mod ├── curl.go ├── .github └── workflows │ └── build.yaml ├── init.go ├── README.md ├── go.sum └── main.go /embed/bypassList: -------------------------------------------------------------------------------- 1 | dummy -------------------------------------------------------------------------------- /embed/specList: -------------------------------------------------------------------------------- 1 | dummy -------------------------------------------------------------------------------- /embed/tldnList: -------------------------------------------------------------------------------- 1 | dummy -------------------------------------------------------------------------------- /embed/VERSION: -------------------------------------------------------------------------------- 1 | 1970-01-01 2 | -------------------------------------------------------------------------------- /internal/README.md: -------------------------------------------------------------------------------- 1 | https://github.com/AdguardTeam/dnsproxy/blob/v0.76.2/internal -------------------------------------------------------------------------------- /internal/cmd/const.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | const ( 4 | Version = "v0.78.0" // nolint:gochecknoglobals 5 | ) 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | vendor 15 | release 16 | build 17 | aiodns 18 | main 19 | -------------------------------------------------------------------------------- /internal/netutil/paths.go: -------------------------------------------------------------------------------- 1 | package netutil 2 | 3 | // DefaultHostsPaths returns the slice of default paths to system hosts files. 4 | // 5 | // TODO(s.chzhen): Since [fs.FS] is no longer needed, update the 6 | // [hostsfile.DefaultHostsPaths] from golibs. 7 | func DefaultHostsPaths() (paths []string, err error) { 8 | return defaultHostsPaths() 9 | } 10 | -------------------------------------------------------------------------------- /internal/netutil/paths_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package netutil 4 | 5 | import ( 6 | "fmt" 7 | "path" 8 | 9 | "golang.org/x/sys/windows" 10 | ) 11 | 12 | // defaultHostsPaths returns default paths to hosts files for Windows. 13 | func defaultHostsPaths() (paths []string, err error) { 14 | sysDir, err := windows.GetSystemDirectory() 15 | if err != nil { 16 | return []string{}, fmt.Errorf("getting system directory: %w", err) 17 | } 18 | 19 | p := path.Join(sysDir, "drivers", "etc", "hosts") 20 | 21 | return []string{p}, nil 22 | } 23 | -------------------------------------------------------------------------------- /internal/netutil/paths_unix.go: -------------------------------------------------------------------------------- 1 | //go:build unix 2 | 3 | package netutil 4 | 5 | import "github.com/AdguardTeam/golibs/hostsfile" 6 | 7 | // defaultHostsPaths returns default paths to hosts files for UNIX. 8 | func defaultHostsPaths() (paths []string, err error) { 9 | paths, err = hostsfile.DefaultHostsPaths() 10 | if err != nil { 11 | // Should not happen because error is always nil. 12 | panic(err) 13 | } 14 | 15 | res := make([]string, 0, len(paths)) 16 | for _, p := range paths { 17 | res = append(res, "/"+p) 18 | } 19 | 20 | return res, nil 21 | } 22 | -------------------------------------------------------------------------------- /internal/handler/ipv6halt.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | // haltAAAA halts the processing of AAAA requests if IPv6 is disabled. req must 10 | // not be nil. 11 | func (h *Default) haltAAAA(ctx context.Context, req *dns.Msg) (resp *dns.Msg) { 12 | if h.isIPv6Halted && req.Question[0].Qtype == dns.TypeAAAA { 13 | h.logger.DebugContext( 14 | ctx, 15 | "ipv6 is disabled; replying with empty response", 16 | "req", req.Question[0].Name, 17 | ) 18 | 19 | return h.messages.NewMsgNODATA(req) 20 | } 21 | 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /internal/netutil/netutil.go: -------------------------------------------------------------------------------- 1 | // Package netutil contains network-related utilities common among dnsproxy 2 | // packages. 3 | // 4 | // TODO(a.garipov): Move improved versions of these into netutil in module 5 | // golibs. 6 | package netutil 7 | 8 | import ( 9 | "net/netip" 10 | "strings" 11 | ) 12 | 13 | // ParseSubnet parses s either as a CIDR prefix itself, or as an IP address, 14 | // returning the corresponding single-IP CIDR prefix. 15 | // 16 | // TODO(e.burkov): Replace usages with [netutil.Prefix]. 17 | func ParseSubnet(s string) (p netip.Prefix, err error) { 18 | if strings.Contains(s, "/") { 19 | p, err = netip.ParsePrefix(s) 20 | if err != nil { 21 | return netip.Prefix{}, err 22 | } 23 | } else { 24 | var ip netip.Addr 25 | ip, err = netip.ParseAddr(s) 26 | if err != nil { 27 | return netip.Prefix{}, err 28 | } 29 | 30 | p = netip.PrefixFrom(ip, ip.BitLen()) 31 | } 32 | 33 | return p, nil 34 | } 35 | -------------------------------------------------------------------------------- /update-list.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | SRC=https://raw.githubusercontent.com/honwen/openwrt-dnsmasq-extra/master/dnsmasq-extra/files/data 6 | 7 | rm -rf embed 8 | mkdir -p embed 9 | 10 | cd embed 11 | curl -fsSL https://raw.githubusercontent.com/honwen/openwrt-dnsmasq-extra/master/dnsmasq-extra/Makefile | sed -n 's+^PKG_VERSION:=++p' >VERSION 12 | curl -fsSLo bypassList.gz https://raw.githubusercontent.com/honwen/openwrt-dnsmasq-extra/master/dnsmasq-extra/files/data/direct.gz 13 | curl -fsSLo tldnList.gz https://raw.githubusercontent.com/honwen/openwrt-dnsmasq-extra/master/dnsmasq-extra/files/data/tldn.gz 14 | curl -fsSLo specList.gz https://raw.githubusercontent.com/honwen/openwrt-dnsmasq-extra/master/dnsmasq-extra/files/data/gfwlist.lite.gz 15 | cd - 16 | 17 | md5sum embed/*.gz 18 | gzip -d embed/*.gz 19 | 20 | echo "# Info: delete somesth" 21 | head=$(sed -ne '/router.asus.com/=' embed/bypassList | tail -n 1) 22 | sed "1,${head}d" -i embed/bypassList 23 | 24 | sed '/^[0-9\.]*$/d' -i embed/* 25 | md5sum embed/* 26 | 27 | echo "# Info: Done" 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24 as builder 2 | 3 | WORKDIR /workdir 4 | 5 | ADD . ./ 6 | 7 | RUN set -ex \ 8 | && GOPROXY='https://mirrors.cloud.tencent.com/go/,direct' go mod download -x \ 9 | && go generate -x \ 10 | && CGO_ENABLED=0 go build -v -ldflags "-X main.VersionString=$(curl -sSL https://api.github.com/repos/honwen/aiodns/commits/master | \ 11 | sed -n '{/sha/p; /date/p;}' | sed 's/.* \"//g' | cut -c1-10 | tr '[:lower:]' '[:upper:]' | sed 'N;s/\n/@/g' | head -1)" . \ 12 | && ./aiodns -v 13 | 14 | FROM chenhw2/alpine:base 15 | LABEL MAINTAINER honwen 16 | 17 | # /usr/bin/aiodns 18 | COPY --from=builder /workdir/aiodns /usr/bin 19 | 20 | USER nobody 21 | 22 | ENV PORT=5300 \ 23 | ARGS="-C -F -A -R -V -L=https://raw.githubusercontents.com/honwen/openwrt-dnsmasq-extra/master/dnsmasq-extra/files/data/gfwlist -L=https://raw.githubusercontents.com/honwen/openwrt-dnsmasq-extra/master/dnsmasq-extra/files/data/tldn -L=https://raw.githubusercontents.com/Loyalsoldier/v2ray-rules-dat/release/greatfire.txt" 24 | 25 | CMD aiodns -l=:${PORT} ${ARGS} 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2024 honwen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME=aiodns 2 | BASE_BUILDDIR=build 3 | BUILDNAME=$(GOOS)-$(GOARCH)$(GOAMD64)$(GOARM) 4 | BUILDDIR=$(BASE_BUILDDIR)/$(BUILDNAME) 5 | VERSION?=dev 6 | 7 | ifeq ($(GOOS),windows) 8 | ext=.exe 9 | archiveCmd=zip -9 -r $(NAME)-$(BUILDNAME)-$(VERSION).zip $(BUILDNAME) 10 | else 11 | ext= 12 | archiveCmd=tar czpvf $(NAME)-$(BUILDNAME)-$(VERSION).tar.gz $(BUILDNAME) 13 | endif 14 | 15 | .PHONY: default 16 | default: build 17 | 18 | build: clean test 19 | go build -mod=vendor 20 | 21 | release: check-env-release 22 | mkdir -p $(BUILDDIR) 23 | cp LICENSE $(BUILDDIR)/ 24 | cp README.md $(BUILDDIR)/ 25 | CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) go build -mod=vendor -ldflags "-s -w -X main.Version=$(VERSION)" -o $(BUILDDIR)/$(NAME)$(ext) 26 | cd $(BASE_BUILDDIR) ; $(archiveCmd) 27 | 28 | test: 29 | CGO_ENABLED=1 go test -race -v -bench=. ./... 30 | 31 | clean: 32 | go clean 33 | rm -rf $(BASE_BUILDDIR) 34 | 35 | check-env-release: 36 | @ if [ "$(GOOS)" = "" ]; then \ 37 | echo "Environment variable GOOS not set"; \ 38 | exit 1; \ 39 | fi 40 | @ if [ "$(GOARCH)" = "" ]; then \ 41 | echo "Environment variable GOOS not set"; \ 42 | exit 1; \ 43 | fi 44 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/honwen/aiodns 2 | 3 | go 1.25.4 4 | 5 | require ( 6 | github.com/AdguardTeam/dnsproxy v0.78.0 7 | github.com/AdguardTeam/golibs v0.35.2 8 | github.com/Workiva/go-datastructures v1.1.7 9 | github.com/ameshkov/dnscrypt/v2 v2.4.0 10 | github.com/miekg/dns v1.1.68 11 | github.com/urfave/cli v1.22.17 12 | golang.org/x/sys v0.38.0 13 | gopkg.in/yaml.v3 v3.0.1 14 | ) 15 | 16 | require ( 17 | github.com/ameshkov/dnsstamps v1.0.3 // indirect 18 | github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 // indirect 19 | github.com/bluele/gcache v0.0.2 // indirect 20 | github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect 21 | github.com/kr/text v0.2.0 // indirect 22 | github.com/patrickmn/go-cache v2.1.0+incompatible // indirect 23 | github.com/quic-go/qpack v0.6.0 // indirect 24 | github.com/quic-go/quic-go v0.57.0 // indirect 25 | github.com/robfig/cron/v3 v3.0.1 // indirect 26 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 27 | golang.org/x/crypto v0.45.0 // indirect 28 | golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect 29 | golang.org/x/mod v0.30.0 // indirect 30 | golang.org/x/net v0.47.0 // indirect 31 | golang.org/x/sync v0.18.0 // indirect 32 | golang.org/x/text v0.31.0 // indirect 33 | golang.org/x/tools v0.39.0 // indirect 34 | gonum.org/v1/gonum v0.16.0 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /curl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/AdguardTeam/dnsproxy/proxy" 11 | "github.com/AdguardTeam/dnsproxy/upstream" 12 | ) 13 | 14 | var tcpTimeout = 30 * time.Second 15 | 16 | func curl(url string, resolvers []string, retry int) (data []byte, err error) { 17 | client := &http.Client{ 18 | Transport: &http.Transport{ 19 | ResponseHeaderTimeout: tcpTimeout, 20 | IdleConnTimeout: tcpTimeout, 21 | DisableKeepAlives: true, 22 | }, 23 | Timeout: tcpTimeout, 24 | } 25 | dialer := &net.Dialer{ 26 | Timeout: tcpTimeout, 27 | DualStack: true, 28 | } 29 | bootUpstreams := []upstream.Upstream{} 30 | for _, it := range resolvers { 31 | if b, err := upstream.AddressToUpstream(it, &upstream.Options{Timeout: tcpTimeout}); err == nil { 32 | bootUpstreams = append(bootUpstreams, b) 33 | } 34 | } 35 | 36 | if len(bootUpstreams) > 0 { 37 | bootUpstreamResolver, _ := proxy.New(&proxy.Config{ 38 | UpstreamMode: proxy.UpstreamModeParallel, 39 | UpstreamConfig: &proxy.UpstreamConfig{ 40 | Upstreams: bootUpstreams, 41 | }, 42 | }) 43 | 44 | defer func() { 45 | bootUpstreams = nil 46 | bootUpstreamResolver = nil 47 | }() 48 | 49 | client.Transport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { 50 | host, port, _ := net.SplitHostPort(addr) 51 | if addrs, err := bootUpstreamResolver.LookupNetIP(ctx, "ip", host); err == nil { 52 | for _, v := range addrs { 53 | if v.IsValid() { 54 | addr = net.JoinHostPort(v.String(), port) 55 | break 56 | } 57 | } 58 | } else { 59 | return nil, err 60 | } 61 | return dialer.DialContext(ctx, network, addr) 62 | } 63 | } 64 | 65 | request, _ := http.NewRequest("GET", url, nil) 66 | if resp, httpErr := client.Do(request); httpErr != nil { 67 | err = httpErr 68 | if retry <= 0 { 69 | return 70 | } else { 71 | return curl(url, resolvers, retry-1) 72 | } 73 | } else { 74 | data, err = io.ReadAll(resp.Body) 75 | resp.Body.Close() 76 | } 77 | return 78 | } 79 | -------------------------------------------------------------------------------- /internal/cmd/tls.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | // NewTLSConfig returns the TLS config that includes a certificate. Use it for 10 | // server TLS Configuration or for a client certificate. If caPath is empty, 11 | // system CAs will be used. 12 | func newTLSConfig(conf *Configuration) (c *tls.Config, err error) { 13 | // Set default TLS min/max versions 14 | tlsMinVersion := tls.VersionTLS10 15 | tlsMaxVersion := tls.VersionTLS13 16 | 17 | switch conf.TLSMinVersion { 18 | case 1.1: 19 | tlsMinVersion = tls.VersionTLS11 20 | case 1.2: 21 | tlsMinVersion = tls.VersionTLS12 22 | case 1.3: 23 | tlsMinVersion = tls.VersionTLS13 24 | } 25 | 26 | switch conf.TLSMaxVersion { 27 | case 1.0: 28 | tlsMaxVersion = tls.VersionTLS10 29 | case 1.1: 30 | tlsMaxVersion = tls.VersionTLS11 31 | case 1.2: 32 | tlsMaxVersion = tls.VersionTLS12 33 | } 34 | 35 | cert, err := loadX509KeyPair(conf.TLSCertPath, conf.TLSKeyPath) 36 | if err != nil { 37 | return nil, fmt.Errorf("loading TLS cert: %s", err) 38 | } 39 | 40 | // #nosec G402 -- TLS MinVersion is configured by user. 41 | return &tls.Config{ 42 | Certificates: []tls.Certificate{cert}, 43 | MinVersion: uint16(tlsMinVersion), 44 | MaxVersion: uint16(tlsMaxVersion), 45 | }, nil 46 | } 47 | 48 | // loadX509KeyPair reads and parses a public/private key pair from a pair of 49 | // files. The files must contain PEM encoded data. The certificate file may 50 | // contain intermediate certificates following the leaf certificate to form a 51 | // certificate chain. On successful return, Certificate.Leaf will be nil 52 | // because the parsed form of the certificate is not retained. 53 | func loadX509KeyPair(certFile, keyFile string) (crt tls.Certificate, err error) { 54 | // #nosec G304 -- Trust the file path that is given in the Configuration. 55 | certPEMBlock, err := os.ReadFile(certFile) 56 | if err != nil { 57 | return tls.Certificate{}, err 58 | } 59 | 60 | // #nosec G304 -- Trust the file path that is given in the Configuration. 61 | keyPEMBlock, err := os.ReadFile(keyFile) 62 | if err != nil { 63 | return tls.Certificate{}, err 64 | } 65 | 66 | return tls.X509KeyPair(certPEMBlock, keyPEMBlock) 67 | } 68 | -------------------------------------------------------------------------------- /internal/handler/default.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | 7 | "github.com/AdguardTeam/dnsproxy/proxy" 8 | "github.com/AdguardTeam/golibs/hostsfile" 9 | ) 10 | 11 | // DefaultConfig is the configuration for [Default]. 12 | type DefaultConfig struct { 13 | // MessageConstructor constructs DNS messages. It must not be nil. 14 | MessageConstructor proxy.MessageConstructor 15 | 16 | // Logger is the logger. It must not be nil. 17 | Logger *slog.Logger 18 | 19 | // HostsFiles is the index containing the records of the hosts files. It 20 | // must not be nil. 21 | HostsFiles hostsfile.Storage 22 | 23 | // HaltIPv6 halts the processing of AAAA requests and makes the handler 24 | // reply with NODATA to them, if true. 25 | HaltIPv6 bool 26 | } 27 | 28 | // Default implements the default configurable [proxy.RequestHandler]. 29 | type Default struct { 30 | messages messageConstructor 31 | hosts hostsfile.Storage 32 | logger *slog.Logger 33 | isIPv6Halted bool 34 | } 35 | 36 | // NewDefault creates a new [Default] handler. 37 | func NewDefault(conf *DefaultConfig) (d *Default) { 38 | mc, ok := conf.MessageConstructor.(messageConstructor) 39 | if !ok { 40 | mc = defaultConstructor{ 41 | MessageConstructor: conf.MessageConstructor, 42 | } 43 | } 44 | 45 | return &Default{ 46 | logger: conf.Logger, 47 | isIPv6Halted: conf.HaltIPv6, 48 | messages: mc, 49 | hosts: conf.HostsFiles, 50 | } 51 | } 52 | 53 | // HandleRequest resolves the DNS request within proxyCtx. It only calls 54 | // [proxy.Proxy.Resolve] if the request isn't handled by any of the internal 55 | // handlers. 56 | func (h *Default) HandleRequest(p *proxy.Proxy, proxyCtx *proxy.DNSContext) (err error) { 57 | // TODO(e.burkov): Use the [*context.Context] instead of 58 | // [*proxy.DNSContext] when the interface-based handler is implemented. 59 | ctx := context.TODO() 60 | 61 | h.logger.DebugContext(ctx, "handling request", "req", &proxyCtx.Req.Question[0]) 62 | 63 | if proxyCtx.Res = h.haltAAAA(ctx, proxyCtx.Req); proxyCtx.Res != nil { 64 | return nil 65 | } 66 | 67 | if proxyCtx.Res = h.resolveFromHosts(ctx, proxyCtx.Req); proxyCtx.Res != nil { 68 | return nil 69 | } 70 | 71 | return p.Resolve(proxyCtx) 72 | } 73 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | "on": 4 | "push": 5 | "tags": 6 | - "v*" 7 | "branches": 8 | - "*" 9 | "pull_request": 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | env: 17 | GO111MODULE: "on" 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - uses: actions/setup-go@v2 22 | with: 23 | go-version: "1.25.4" 24 | 25 | - name: Prepare environment 26 | run: |- 27 | RELEASE_VERSION="${GITHUB_REF##*/}" 28 | if [[ "${RELEASE_VERSION}" != v* ]]; then RELEASE_VERSION='dev'; fi 29 | echo "RELEASE_VERSION=\"${RELEASE_VERSION}@${GITHUB_SHA:0:10}\"" | tee -a $GITHUB_ENV 30 | go mod vendor 31 | go generate -x 32 | 33 | # Win 34 | - run: GOOS=windows GOARCH=386 VERSION=${RELEASE_VERSION} make release 35 | - run: GOOS=windows GOARCH=amd64 VERSION=${RELEASE_VERSION} make release 36 | - run: GOOS=windows GOARCH=amd64 GOAMD64=v3 VERSION=${RELEASE_VERSION} make release 37 | - run: GOOS=windows GOARCH=arm64 VERSION=${RELEASE_VERSION} make release 38 | 39 | # MacOS 40 | - run: GOOS=darwin GOARCH=amd64 VERSION=${RELEASE_VERSION} make release 41 | - run: GOOS=darwin GOARCH=amd64 GOAMD64=v3 VERSION=${RELEASE_VERSION} make release 42 | - run: GOOS=darwin GOARCH=arm64 VERSION=${RELEASE_VERSION} make release 43 | 44 | # Linux X86/AMD64 45 | - run: GOOS=linux GOARCH=386 VERSION=${RELEASE_VERSION} make release 46 | - run: GOOS=linux GOARCH=amd64 VERSION=${RELEASE_VERSION} make release 47 | - run: GOOS=linux GOARCH=amd64 GOAMD64=v3 VERSION=${RELEASE_VERSION} make release 48 | 49 | # Linux ARM 50 | - run: GOOS=linux GOARCH=arm GOARM=6 VERSION=${RELEASE_VERSION} make release 51 | - run: GOOS=linux GOARCH=arm64 VERSION=${RELEASE_VERSION} make release 52 | 53 | # Linux MIPS/MIPSLE 54 | - run: GOOS=linux GOARCH=mips GOMIPS=softfloat VERSION=${RELEASE_VERSION} make release 55 | - run: GOOS=linux GOARCH=mipsle GOMIPS=softfloat VERSION=${RELEASE_VERSION} make release 56 | 57 | # FreeBSD X86 58 | - run: GOOS=freebsd GOARCH=386 VERSION=${RELEASE_VERSION} make release 59 | - run: GOOS=freebsd GOARCH=amd64 VERSION=${RELEASE_VERSION} make release 60 | - run: GOOS=freebsd GOARCH=amd64 GOAMD64=v3 VERSION=${RELEASE_VERSION} make release 61 | 62 | # FreeBSD ARM/ARM64 63 | - run: GOOS=freebsd GOARCH=arm GOARM=6 VERSION=${RELEASE_VERSION} make release 64 | - run: GOOS=freebsd GOARCH=arm64 VERSION=${RELEASE_VERSION} make release 65 | 66 | - run: ls -l build/aiodns-* 67 | 68 | - name: Create release 69 | if: startsWith(github.ref, 'refs/tags/v') 70 | id: create_release 71 | uses: ncipollo/release-action@v1 72 | with: 73 | allowUpdates: true 74 | artifacts: "build/aiodns-*" 75 | token: ${{ secrets.GITHUB_TOKEN }} 76 | -------------------------------------------------------------------------------- /init.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "strings" 6 | 7 | "github.com/Workiva/go-datastructures/set" 8 | ) 9 | 10 | //go:generate ./update-list.sh 11 | 12 | //go:embed embed/VERSION 13 | var embedDate string 14 | 15 | //go:embed embed/tldnList 16 | var tldnList string 17 | 18 | //go:embed embed/bypassList 19 | var bypassList string 20 | 21 | //go:embed embed/specList 22 | var specList string 23 | 24 | func init() { 25 | embedDate = strings.TrimSpace(embedDate) 26 | 27 | defaultUpstream.Set("tls://dot.pub") 28 | defaultUpstream.Set("tls://dns.alidns.com") 29 | defaultUpstream.Set("https://doh.pub/dns-query") 30 | defaultUpstream.Set("https://dns.alidns.com/dns-query") 31 | 32 | specUpstream.Set("https://v.recipes/dns-query") 33 | specUpstream.Set("https://0ms.dev/dns-query") 34 | // specUpstream.Set("quic://dns.adguard.com") 35 | // specUpstream.Set("https://odvr.nic.cz/doh") 36 | // specUpstream.Set("https://doh.opendns.com/dns-query") 37 | // specUpstream.Set("https://8.8.8.8/dns-query") 38 | // specUpstream.Set("https://9.9.9.11/dns-query") 39 | // specUpstream.Set("https://cloudflare-dns.com/dns-query") 40 | // specUpstream.Set("https://149.112.112.11/dns-query") 41 | // specUpstream.Set("https://149.112.112.11:5053/dns-query") 42 | specUpstream.Set("sdns://AQEAAAAAAAAADjIwOC42Ny4yMjAuMjIwILc1EUAgbyJdPivYItf9aR6hwzzI1maNDL4Ev6vKQ_t5GzIuZG5zY3J5cHQtY2VydC5vcGVuZG5zLmNvbQ") 43 | specUpstream.Set("sdns://AQQAAAAAAAAAEDc3Ljg4LjguNzg6MTUzNTMg04TAccn3RmKvKszVe13MlxTUB7atNgHhrtwG1W1JYyciMi5kbnNjcnlwdC1jZXJ0LmJyb3dzZXIueWFuZGV4Lm5ldA") 44 | 45 | // fallUpstream.Set("tcp://9.9.9.11:9953") 46 | // fallUpstream.Set("tcp://149.112.112.11:9953") 47 | fallUpstream.Set("https://v.recipes/dns-query") 48 | fallUpstream.Set("https://dns.controld.com/comss") 49 | fallUpstream.Set("https://101.102.103.104/dns-query") 50 | fallUpstream.Set("https://max.rethinkdns.com/dns-query") 51 | // fallUpstream.Set("tls://dns.rubyfish.cn") 52 | // fallUpstream.Set("https://dns.rubyfish.cn/dns-query") 53 | // fallUpstream.Set("https://1.15.50.48/verse") 54 | // fallUpstream.Set("https://106.52.218.142/verse") 55 | 56 | bootUpstream.Set("tls://223.5.5.5") 57 | bootUpstream.Set("tls://1.12.12.12") 58 | bootUpstream.Set("tcp://180.184.1.1") 59 | bootUpstream.Set("udp://180.184.2.2") 60 | bootUpstream.Set("tcp://114.114.114.114") 61 | bootUpstream.Set("udp://114.114.115.115") 62 | } 63 | 64 | var initSpecDomains = set.New( 65 | "dl.google.com", 66 | "googleapis.cn", 67 | "googleapis.com", 68 | "gstatic.com", 69 | ) 70 | 71 | var initSpecUpstreams = []string{ 72 | "https://v.recipes/dns-query", 73 | "https://odvr.nic.cz/doh", 74 | "https://doh.dns.sb/dns-query", 75 | // "https://dns.twnic.tw/dns-query", 76 | "https://dns.adguard.com/dns-query", 77 | "https://doh.opendns.com/dns-query", 78 | "sdns://AQUAAAAAAAAACjguMjAuMjQ3LjIg0sJUqpYcHsoXmZb1X7yAHwg2xyN5q1J-zaiGG-Dgs7AoMi5kbnNjcnlwdC1jZXJ0LnNoaWVsZC0yLmRuc2J5Y29tb2RvLmNvbQ", 79 | "sdns://AQMAAAAAAAAAFDE3Ni4xMDMuMTMwLjEzMDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AIO DNS 2 | 3 | A All-In-One DNS Solution, A Specail-List Rule Generator of `AdguardTeam/dnsproxy` 4 | 5 | ### Source 6 | 7 | - https://github.com/honwen/aiodns 8 | 9 | ### Thanks 10 | 11 | - https://github.com/AdguardTeam/dnsproxy 12 | - https://github.com/honwen/openwrt-dnsmasq-extra 13 | 14 | ### Docker 15 | 16 | - https://hub.docker.com/r/chenhw2/aiodns 17 | 18 | ### Usage 19 | 20 | ```bash 21 | $ docker pull chenhw2/aiodns 22 | 23 | $ docker run -d \ 24 | ---network=host \ 25 | -e PORT=53 \ 26 | chenhw2/aiodns 27 | ``` 28 | 29 | ### Help 30 | 31 | ```bash 32 | $ docker run --rm chenhw2/aiodns -h 33 | NAME: 34 | AIO DNS - All In One Clean DNS Solution. 35 | 36 | USAGE: 37 | aiodns [global options] command [command options] [arguments...] 38 | 39 | VERSION: 40 | Git:[MISSING BUILD VERSION [GIT HASH]] (dnsproxy version)(go version) 41 | 42 | COMMANDS: 43 | help, h Shows a list of commands or help for one command 44 | 45 | GLOBAL OPTIONS: 46 | --listen value, -l value Listening address (default: ":5300") 47 | --upstream value, -u value An upstream to be default used (can be specified multiple times) (default: "tls://dns.pub", ...) 48 | --special-upstream value, -U value An upstream to be special used (can be specified multiple times) (default: "https://8.8.8.8/dns-query", ...) 49 | --fallback value, -f value Fallback resolvers to use when regular ones are unavailable, can be specified multiple times (default: "tcp://9.9.9.11:9953", ...) 50 | --bootstrap value, -b value Bootstrap DNS for DoH and DoT, can be specified multiple times (default: "tls://223.5.5.5", "tls://1.12.12.12", ...) 51 | --special-list value, -L value List of domains using special-upstream (can be specified multiple times), (file path from local or net) 52 | --bypass-list value, -B value List of domains bypass special-upstream (can be specified multiple times), (file path from local or net) 53 | --edns value, -e value Send EDNS Client Address to default upstreams 54 | --timeout value, -t value Timeout of Each upstream, [1, 59] seconds (default: 3) 55 | --cache, -C If specified, DNS cache is enabled 56 | --insecure, -I If specified, disable SSL/TLS Certificate check (for some OS without ca-certificates) 57 | --ipv6-disabled, -R If specified, all AAAA requests will be replied with NoError RCode and empty answer 58 | --refuse-any, -A If specified, refuse ANY requests 59 | --fastest-addr, -F If specified, Respond to A or AAAA requests only with the fastest IP address 60 | --http3, -H If specified, Enable HTTP/3 support 61 | --verbose, -V If specified, Verbose output 62 | --help, -h show help 63 | --version, -v print the version 64 | ``` 65 | 66 | ### Example 67 | 68 | - use latest list-file fetching online 69 | ```bash 70 | aiodns --special-list=https://raw.githubusercontent.com/honwen/openwrt-dnsmasq-extra/master/dnsmasq-extra/files/data/tldn.gz 71 | ``` 72 | 73 | - use list-file locally 74 | ```bash 75 | aiodns --special-list=/data/tldn 76 | ``` 77 | -------------------------------------------------------------------------------- /internal/dnsmsg/constructor.go: -------------------------------------------------------------------------------- 1 | // Package dnsmsg contains common constants, functions, and types for inspecting 2 | // and constructing DNS messages. 3 | package dnsmsg 4 | 5 | import ( 6 | "strings" 7 | 8 | "github.com/miekg/dns" 9 | ) 10 | 11 | // MessageConstructor creates DNS messages. 12 | type MessageConstructor interface { 13 | // NewMsgNXDOMAIN creates a new response message replying to req with the 14 | // NXDOMAIN code. 15 | NewMsgNXDOMAIN(req *dns.Msg) (resp *dns.Msg) 16 | 17 | // NewMsgSERVFAIL creates a new response message replying to req with the 18 | // SERVFAIL code. 19 | NewMsgSERVFAIL(req *dns.Msg) (resp *dns.Msg) 20 | 21 | // NewMsgNOTIMPLEMENTED creates a new response message replying to req with 22 | // the NOTIMPLEMENTED code. 23 | NewMsgNOTIMPLEMENTED(req *dns.Msg) (resp *dns.Msg) 24 | 25 | // NewMsgNODATA creates a new empty response message replying to req with 26 | // the NOERROR code. 27 | // 28 | // See https://www.rfc-editor.org/rfc/rfc2308#section-2.2. 29 | NewMsgNODATA(req *dns.Msg) (resp *dns.Msg) 30 | } 31 | 32 | // DefaultMessageConstructor is a default implementation of 33 | // [MessageConstructor]. 34 | type DefaultMessageConstructor struct{} 35 | 36 | // type check 37 | var _ MessageConstructor = DefaultMessageConstructor{} 38 | 39 | // NewMsgNXDOMAIN implements the [MessageConstructor] interface for 40 | // DefaultMessageConstructor. 41 | func (DefaultMessageConstructor) NewMsgNXDOMAIN(req *dns.Msg) (resp *dns.Msg) { 42 | return reply(req, dns.RcodeNameError) 43 | } 44 | 45 | // NewMsgSERVFAIL implements the [MessageConstructor] interface for 46 | // DefaultMessageConstructor. 47 | func (DefaultMessageConstructor) NewMsgSERVFAIL(req *dns.Msg) (resp *dns.Msg) { 48 | return reply(req, dns.RcodeServerFailure) 49 | } 50 | 51 | // NewMsgNOTIMPLEMENTED implements the [MessageConstructor] interface for 52 | // DefaultMessageConstructor. 53 | func (DefaultMessageConstructor) NewMsgNOTIMPLEMENTED(req *dns.Msg) (resp *dns.Msg) { 54 | resp = reply(req, dns.RcodeNotImplemented) 55 | 56 | // Most of the Internet and especially the inner core has an MTU of at least 57 | // 1500 octets. Maximum DNS/UDP payload size for IPv6 on MTU 1500 ethernet 58 | // is 1452 (1500 minus 40 (IPv6 header size) minus 8 (UDP header size)). 59 | // 60 | // See appendix A of https://datatracker.ietf.org/doc/draft-ietf-dnsop-avoid-fragmentation/17. 61 | const maxUDPPayload = 1452 62 | 63 | // NOTIMPLEMENTED without EDNS is treated as 'we don't support EDNS', so 64 | // explicitly set it. 65 | resp.SetEdns0(maxUDPPayload, false) 66 | 67 | return resp 68 | } 69 | 70 | // NewMsgNODATA implements the [MessageConstructor] interface for 71 | // DefaultMessageConstructor. 72 | func (DefaultMessageConstructor) NewMsgNODATA(req *dns.Msg) (resp *dns.Msg) { 73 | resp = reply(req, dns.RcodeSuccess) 74 | 75 | zone := req.Question[0].Name 76 | soa := &dns.SOA{ 77 | // Values copied from verisign's nonexistent .com domain. 78 | // 79 | // Their exact values are not important in our use case because they are 80 | // used for domain transfers between primary/secondary DNS servers. 81 | Refresh: 1800, 82 | Retry: 60, 83 | Expire: 604800, 84 | Minttl: 86400, 85 | // copied from AdGuard DNS 86 | Ns: "fake-for-negative-caching.adguard.com.", 87 | Serial: 100500, 88 | Mbox: "hostmaster.", 89 | // rest is request-specific 90 | Hdr: dns.RR_Header{ 91 | Name: zone, 92 | Rrtype: dns.TypeSOA, 93 | Ttl: 10, 94 | Class: dns.ClassINET, 95 | }, 96 | } 97 | 98 | if !strings.HasPrefix(zone, ".") { 99 | soa.Mbox += zone 100 | } 101 | 102 | resp.Ns = append(resp.Ns, soa) 103 | 104 | return resp 105 | } 106 | 107 | // reply creates a new response message replying to req with the given code. 108 | func reply(req *dns.Msg, code int) (resp *dns.Msg) { 109 | resp = (&dns.Msg{}).SetRcode(req, code) 110 | resp.RecursionAvailable = true 111 | 112 | return resp 113 | } 114 | -------------------------------------------------------------------------------- /internal/handler/hosts.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "net/netip" 8 | "os" 9 | "slices" 10 | "strings" 11 | 12 | "github.com/AdguardTeam/golibs/errors" 13 | "github.com/AdguardTeam/golibs/hostsfile" 14 | "github.com/AdguardTeam/golibs/logutil/slogutil" 15 | "github.com/AdguardTeam/golibs/netutil" 16 | "github.com/miekg/dns" 17 | ) 18 | 19 | // emptyStorage is a [hostsfile.Storage] that contains no records. 20 | // 21 | // TODO(e.burkov): Move to [hostsfile]. 22 | type emptyStorage [0]hostsfile.Record 23 | 24 | // type check 25 | var _ hostsfile.Storage = emptyStorage{} 26 | 27 | // ByAddr implements the [hostsfile.Storage] interface for [emptyStorage]. 28 | func (emptyStorage) ByAddr(_ netip.Addr) (names []string) { 29 | return nil 30 | } 31 | 32 | // ByName implements the [hostsfile.Storage] interface for [emptyStorage]. 33 | func (emptyStorage) ByName(_ string) (addrs []netip.Addr) { 34 | return nil 35 | } 36 | 37 | // ReadHosts reads the hosts files from the file system and returns a storage 38 | // with parsed records. strg is always usable even if an error occurred. 39 | func ReadHosts( 40 | ctx context.Context, 41 | l *slog.Logger, 42 | paths []string, 43 | ) (strg hostsfile.Storage, err error) { 44 | // Don't check the error since it may only appear when any readers used. 45 | defaultStrg, _ := hostsfile.NewDefaultStorage(ctx, &hostsfile.DefaultStorageConfig{ 46 | Logger: l, 47 | }) 48 | 49 | var errs []error 50 | for _, path := range paths { 51 | err = readHostsFile(ctx, defaultStrg, path) 52 | if err != nil { 53 | // Don't wrap the error since it's informative enough as is. 54 | errs = append(errs, err) 55 | } 56 | } 57 | 58 | // TODO(e.burkov): Add method for length. 59 | isEmpty := true 60 | defaultStrg.RangeAddrs(func(_ string, _ []netip.Addr) (cont bool) { 61 | isEmpty = false 62 | 63 | return false 64 | }) 65 | 66 | if isEmpty { 67 | return emptyStorage{}, errors.Join(errs...) 68 | } 69 | 70 | return defaultStrg, errors.Join(errs...) 71 | } 72 | 73 | // readHostsFile reads the hosts file at path and parses it into strg. 74 | func readHostsFile(ctx context.Context, strg *hostsfile.DefaultStorage, path string) (err error) { 75 | // #nosec G304 -- Trust the file path from the configuration file. 76 | f, err := os.Open(path) 77 | if err != nil { 78 | // Don't wrap the error since it's informative enough as is. 79 | return err 80 | } 81 | 82 | defer func() { err = errors.WithDeferred(err, f.Close()) }() 83 | 84 | err = hostsfile.Parse(ctx, strg, f, nil) 85 | if err != nil { 86 | return fmt.Errorf("parsing hosts file %q: %w", path, err) 87 | } 88 | 89 | return nil 90 | } 91 | 92 | // resolveFromHosts resolves the DNS query from the hosts file. It fills the 93 | // response with the A, AAAA, and PTR records from the hosts file. 94 | func (h *Default) resolveFromHosts(ctx context.Context, req *dns.Msg) (resp *dns.Msg) { 95 | var addrs []netip.Addr 96 | var ptrs []string 97 | 98 | q := req.Question[0] 99 | name := strings.TrimSuffix(q.Name, ".") 100 | switch q.Qtype { 101 | case dns.TypeA: 102 | addrs = slices.Clone(h.hosts.ByName(name)) 103 | addrs = slices.DeleteFunc(addrs, netip.Addr.Is6) 104 | case dns.TypeAAAA: 105 | addrs = slices.Clone(h.hosts.ByName(name)) 106 | addrs = slices.DeleteFunc(addrs, netip.Addr.Is4) 107 | case dns.TypePTR: 108 | addr, err := netutil.IPFromReversedAddr(name) 109 | if err != nil { 110 | h.logger.DebugContext(ctx, "failed parsing ptr", slogutil.KeyError, err) 111 | 112 | return nil 113 | } 114 | 115 | ptrs = h.hosts.ByAddr(addr) 116 | default: 117 | return nil 118 | } 119 | 120 | switch { 121 | case len(addrs) > 0: 122 | resp = h.messages.NewIPResponse(req, addrs) 123 | case len(ptrs) > 0: 124 | resp = h.messages.NewCompressedResponse(req, dns.RcodeSuccess) 125 | name = req.Question[0].Name 126 | for _, ptr := range ptrs { 127 | resp.Answer = append(resp.Answer, h.messages.NewPTRAnswer(name, dns.Fqdn(ptr))) 128 | } 129 | default: 130 | h.logger.DebugContext(ctx, "no hosts records found", "name", name, "qtype", q.Qtype) 131 | } 132 | 133 | return resp 134 | } 135 | -------------------------------------------------------------------------------- /internal/handler/constructor.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/netip" 5 | 6 | "github.com/AdguardTeam/dnsproxy/proxy" 7 | "github.com/miekg/dns" 8 | ) 9 | 10 | // messageConstructor is an extension of the [proxy.MessageConstructor] 11 | // interface that also provides methods for creating DNS responses. 12 | type messageConstructor interface { 13 | proxy.MessageConstructor 14 | 15 | // NewCompressedResponse creates a new compressed response message for req 16 | // with the given response code. 17 | NewCompressedResponse(req *dns.Msg, code int) (resp *dns.Msg) 18 | 19 | // NewPTRAnswer creates a new resource record for PTR response with the 20 | // given FQDN and PTR domain. Arguments must be fully qualified domain 21 | // names. 22 | NewPTRAnswer(fqdn, ptrFQDN string) (ans *dns.PTR) 23 | 24 | // NewIPResponse creates a new A/AAAA response message for req with the 25 | // given IP addresses. All IP addresses must be of the same family. 26 | NewIPResponse(req *dns.Msg, ips []netip.Addr) (resp *dns.Msg) 27 | } 28 | 29 | // defaultConstructor is a wrapper for [proxy.MessageConstructor] that also 30 | // implements the [messageConstructor] interface. 31 | // 32 | // TODO(e.burkov): This implementation reflects the one from AdGuard Home, 33 | // consider moving it to [golibs]. 34 | type defaultConstructor struct { 35 | proxy.MessageConstructor 36 | } 37 | 38 | // type check 39 | var _ messageConstructor = defaultConstructor{} 40 | 41 | // NewCompressedResponse implements the [messageConstructor] interface for 42 | // defaultConstructor. 43 | func (defaultConstructor) NewCompressedResponse(req *dns.Msg, code int) (resp *dns.Msg) { 44 | resp = reply(req, code) 45 | resp.Compress = true 46 | 47 | return resp 48 | } 49 | 50 | // NewPTRAnswer implements the [messageConstructor] interface for 51 | // [defaultConstructor]. 52 | func (defaultConstructor) NewPTRAnswer(fqdn, ptrFQDN string) (ans *dns.PTR) { 53 | return &dns.PTR{ 54 | Hdr: hdr(fqdn, dns.TypePTR), 55 | Ptr: dns.Fqdn(ptrFQDN), 56 | } 57 | } 58 | 59 | // NewIPResponse implements the [messageConstructor] interface for 60 | // [defaultConstructor] 61 | func (c defaultConstructor) NewIPResponse(req *dns.Msg, ips []netip.Addr) (resp *dns.Msg) { 62 | var ans []dns.RR 63 | switch req.Question[0].Qtype { 64 | case dns.TypeA: 65 | ans = genAnswersWithIPv4s(req, ips) 66 | case dns.TypeAAAA: 67 | for _, ip := range ips { 68 | if ip.Is6() { 69 | ans = append(ans, newAnswerAAAA(req, ip)) 70 | } 71 | } 72 | default: 73 | // Go on and return an empty response. 74 | } 75 | 76 | resp = c.NewCompressedResponse(req, dns.RcodeSuccess) 77 | resp.Answer = ans 78 | 79 | return resp 80 | } 81 | 82 | // defaultResponseTTL is the default TTL for the DNS responses in seconds. 83 | const defaultResponseTTL = 10 84 | 85 | // hdr creates a new DNS header with the given name and RR type. 86 | func hdr(name string, rrType uint16) (h dns.RR_Header) { 87 | return dns.RR_Header{ 88 | Name: name, 89 | Rrtype: rrType, 90 | Ttl: defaultResponseTTL, 91 | Class: dns.ClassINET, 92 | } 93 | } 94 | 95 | // reply creates a DNS response for req. 96 | func reply(req *dns.Msg, code int) (resp *dns.Msg) { 97 | resp = (&dns.Msg{}).SetRcode(req, code) 98 | resp.RecursionAvailable = true 99 | 100 | return resp 101 | } 102 | 103 | // newAnswerA creates a DNS A answer for req with the given IP address. 104 | func newAnswerA(req *dns.Msg, ip netip.Addr) (ans *dns.A) { 105 | return &dns.A{ 106 | Hdr: hdr(req.Question[0].Name, dns.TypeA), 107 | A: ip.AsSlice(), 108 | } 109 | } 110 | 111 | // newAnswerAAAA creates a DNS AAAA answer for req with the given IP address. 112 | func newAnswerAAAA(req *dns.Msg, ip netip.Addr) (ans *dns.AAAA) { 113 | return &dns.AAAA{ 114 | Hdr: hdr(req.Question[0].Name, dns.TypeAAAA), 115 | AAAA: ip.AsSlice(), 116 | } 117 | } 118 | 119 | // genAnswersWithIPv4s generates DNS A answers provided IPv4 addresses. If any 120 | // of the IPs isn't an IPv4 address, genAnswersWithIPv4s logs a warning and 121 | // returns nil, 122 | func genAnswersWithIPv4s(req *dns.Msg, ips []netip.Addr) (ans []dns.RR) { 123 | for _, ip := range ips { 124 | if !ip.Is4() { 125 | return nil 126 | } 127 | 128 | ans = append(ans, newAnswerA(req, ip)) 129 | } 130 | 131 | return ans 132 | } 133 | -------------------------------------------------------------------------------- /internal/cmd/flag.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/AdguardTeam/golibs/stringutil" 10 | ) 11 | 12 | // uint32Value is an uint32 that can be defined as a flag for [flag.FlagSet]. 13 | type uint32Value uint32 14 | 15 | // type check 16 | var _ flag.Value = (*uint32Value)(nil) 17 | 18 | // Set implements the [flag.Value] interface for *uint32Value. 19 | func (i *uint32Value) Set(s string) (err error) { 20 | v, err := strconv.ParseUint(s, 0, 32) 21 | *i = uint32Value(v) 22 | 23 | return err 24 | } 25 | 26 | // String implements the [flag.Value] interface for *uint32Value. 27 | func (i *uint32Value) String() (out string) { 28 | return strconv.FormatUint(uint64(*i), 10) 29 | } 30 | 31 | // float32Value is an float32 that can be defined as a flag for [flag.FlagSet]. 32 | type float32Value float32 33 | 34 | // type check 35 | var _ flag.Value = (*float32Value)(nil) 36 | 37 | // Set implements the [flag.Value] interface for *float32Value. 38 | func (i *float32Value) Set(s string) (err error) { 39 | v, err := strconv.ParseFloat(s, 32) 40 | *i = float32Value(v) 41 | 42 | return err 43 | } 44 | 45 | // String implements the [flag.Value] interface for *float32Value. 46 | func (i *float32Value) String() (out string) { 47 | return strconv.FormatFloat(float64(*i), 'f', 3, 32) 48 | } 49 | 50 | // intSliceValue represent a struct with a slice of integers that can be defined 51 | // as a flag for [flag.FlagSet]. 52 | type intSliceValue struct { 53 | // values is the pointer to a slice of integers to store parsed values. 54 | values *[]int 55 | 56 | // isSet is false until the corresponding flag is met for the first time. 57 | // When the flag is found, the default value is overwritten with zero value. 58 | isSet bool 59 | } 60 | 61 | // newIntSliceValue returns a pointer to intSliceValue with the given value. 62 | func newIntSliceValue(p *[]int) (out *intSliceValue) { 63 | return &intSliceValue{ 64 | values: p, 65 | isSet: false, 66 | } 67 | } 68 | 69 | // type check 70 | var _ flag.Value = (*intSliceValue)(nil) 71 | 72 | // Set implements the [flag.Value] interface for *intSliceValue. 73 | func (i *intSliceValue) Set(s string) (err error) { 74 | v, err := strconv.Atoi(s) 75 | if err != nil { 76 | return fmt.Errorf("parsing integer slice arg %q: %w", s, err) 77 | } 78 | 79 | if !i.isSet { 80 | i.isSet = true 81 | *i.values = []int{} 82 | } 83 | 84 | *i.values = append(*i.values, v) 85 | 86 | return nil 87 | } 88 | 89 | // String implements the [flag.Value] interface for *intSliceValue. 90 | func (i *intSliceValue) String() (out string) { 91 | if i == nil || i.values == nil { 92 | return "" 93 | } 94 | 95 | sb := &strings.Builder{} 96 | for idx, v := range *i.values { 97 | if idx > 0 { 98 | stringutil.WriteToBuilder(sb, ",") 99 | } 100 | 101 | stringutil.WriteToBuilder(sb, strconv.Itoa(v)) 102 | } 103 | 104 | return sb.String() 105 | } 106 | 107 | // stringSliceValue represent a struct with a slice of strings that can be 108 | // defined as a flag for [flag.FlagSet]. 109 | type stringSliceValue struct { 110 | // values is the pointer to a slice of string to store parsed values. 111 | values *[]string 112 | 113 | // isSet is false until the corresponding flag is met for the first time. 114 | // When the flag is found, the default value is overwritten with zero value. 115 | isSet bool 116 | } 117 | 118 | // newStringSliceValue returns a pointer to stringSliceValue with the given 119 | // value. 120 | func newStringSliceValue(p *[]string) (out *stringSliceValue) { 121 | return &stringSliceValue{ 122 | values: p, 123 | isSet: false, 124 | } 125 | } 126 | 127 | // type check 128 | var _ flag.Value = (*stringSliceValue)(nil) 129 | 130 | // Set implements the [flag.Value] interface for *stringSliceValue. 131 | func (i *stringSliceValue) Set(s string) (err error) { 132 | if !i.isSet { 133 | i.isSet = true 134 | *i.values = []string{} 135 | } 136 | 137 | *i.values = append(*i.values, s) 138 | 139 | return nil 140 | } 141 | 142 | // String implements the [flag.Value] interface for *stringSliceValue. 143 | func (i *stringSliceValue) String() (out string) { 144 | if i == nil || i.values == nil { 145 | return "" 146 | } 147 | 148 | sb := &strings.Builder{} 149 | for idx, v := range *i.values { 150 | if idx > 0 { 151 | stringutil.WriteToBuilder(sb, ",") 152 | } 153 | 154 | stringutil.WriteToBuilder(sb, v) 155 | } 156 | 157 | return sb.String() 158 | } 159 | -------------------------------------------------------------------------------- /internal/cmd/cmd.go: -------------------------------------------------------------------------------- 1 | // Package cmd is the dnsproxy CLI entry point. 2 | package cmd 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "log/slog" 8 | "net/http" 9 | "net/http/pprof" 10 | "os" 11 | "os/signal" 12 | "syscall" 13 | "time" 14 | 15 | "github.com/AdguardTeam/dnsproxy/proxy" 16 | "github.com/AdguardTeam/golibs/errors" 17 | "github.com/AdguardTeam/golibs/logutil/slogutil" 18 | "github.com/AdguardTeam/golibs/osutil" 19 | ) 20 | 21 | // Main is the entrypoint of dnsproxy CLI. Main may accept arguments, such as 22 | // embedded assets and command-line arguments. 23 | func Main() { 24 | conf, exitCode, err := parseConfig() 25 | if err != nil { 26 | _, _ = fmt.Fprintln(os.Stderr, fmt.Errorf("parsing options: %w", err)) 27 | } 28 | 29 | if conf == nil { 30 | os.Exit(exitCode) 31 | } 32 | 33 | logOutput := os.Stdout 34 | if conf.LogOutput != "" { 35 | // #nosec G302 -- Trust the file path that is given in the 36 | // Configuration. 37 | logOutput, err = os.OpenFile(conf.LogOutput, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644) 38 | if err != nil { 39 | _, _ = fmt.Fprintln(os.Stderr, fmt.Errorf("cannot create a log file: %s", err)) 40 | 41 | os.Exit(osutil.ExitCodeArgumentError) 42 | } 43 | 44 | defer func() { _ = logOutput.Close() }() 45 | } 46 | 47 | lvl := slog.LevelInfo 48 | if conf.Verbose { 49 | lvl = slog.LevelDebug 50 | } 51 | 52 | l := slogutil.New(&slogutil.Config{ 53 | Output: logOutput, 54 | Format: slogutil.FormatDefault, 55 | Level: lvl, 56 | // TODO(d.kolyshev): Consider making configurable. 57 | AddTimestamp: true, 58 | }) 59 | 60 | ctx := context.Background() 61 | 62 | if conf.Pprof { 63 | runPprof(ctx, l) 64 | } 65 | 66 | err = RunProxy(ctx, l, conf) 67 | if err != nil { 68 | l.ErrorContext(ctx, "running dnsproxy", slogutil.KeyError, err) 69 | 70 | // As defers are skipped in case of os.Exit, close logOutput manually. 71 | // 72 | // TODO(a.garipov): Consider making logger.Close method. 73 | if logOutput != os.Stdout { 74 | _ = logOutput.Close() 75 | } 76 | 77 | os.Exit(osutil.ExitCodeFailure) 78 | } 79 | } 80 | 81 | // RunProxy starts and runs the proxy. l must not be nil. 82 | // 83 | // TODO(e.burkov): Move into separate dnssvc package. 84 | func RunProxy(ctx context.Context, l *slog.Logger, conf *Configuration) (err error) { 85 | l.InfoContext( 86 | ctx, 87 | "dnsproxy starting", 88 | "version", Version, 89 | ) 90 | 91 | // Prepare the proxy server and its Configuration. 92 | proxyConf, err := createProxyConfig(ctx, l, conf) 93 | if err != nil { 94 | return fmt.Errorf("configuring proxy: %w", err) 95 | } 96 | 97 | dnsProxy, err := proxy.New(proxyConf) 98 | if err != nil { 99 | return fmt.Errorf("creating proxy: %w", err) 100 | } 101 | 102 | // Start the proxy server. 103 | err = dnsProxy.Start(ctx) 104 | if err != nil { 105 | return fmt.Errorf("starting dnsproxy: %w", err) 106 | } 107 | 108 | // TODO(e.burkov): Use [service.SignalHandler]. 109 | signalChannel := make(chan os.Signal, 1) 110 | signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM) 111 | <-signalChannel 112 | 113 | // Stopping the proxy. 114 | err = dnsProxy.Shutdown(ctx) 115 | if err != nil { 116 | return fmt.Errorf("stopping dnsproxy: %w", err) 117 | } 118 | 119 | return nil 120 | } 121 | 122 | // runPprof runs pprof server on localhost:6060. 123 | // 124 | // TODO(e.burkov): Add debugsvc. 125 | func runPprof(ctx context.Context, l *slog.Logger) { 126 | mux := http.NewServeMux() 127 | mux.HandleFunc("/debug/pprof/", pprof.Index) 128 | mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) 129 | mux.HandleFunc("/debug/pprof/profile", pprof.Profile) 130 | mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) 131 | mux.HandleFunc("/debug/pprof/trace", pprof.Trace) 132 | mux.Handle("/debug/pprof/allocs", pprof.Handler("allocs")) 133 | mux.Handle("/debug/pprof/block", pprof.Handler("block")) 134 | mux.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine")) 135 | mux.Handle("/debug/pprof/heap", pprof.Handler("heap")) 136 | mux.Handle("/debug/pprof/mutex", pprof.Handler("mutex")) 137 | mux.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate")) 138 | 139 | go func() { 140 | // TODO(d.kolyshev): Consider making configurable. 141 | const pprofAddr = "localhost:6060" 142 | l.InfoContext(ctx, "starting pprof", "addr", pprofAddr) 143 | 144 | srv := &http.Server{ 145 | Addr: pprofAddr, 146 | ReadTimeout: 60 * time.Second, 147 | Handler: mux, 148 | } 149 | 150 | err := srv.ListenAndServe() 151 | if err != nil && !errors.Is(err, http.ErrServerClosed) { 152 | l.ErrorContext(ctx, "pprof failed to listen", "addr", pprofAddr, slogutil.KeyError, err) 153 | } 154 | }() 155 | } 156 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/AdguardTeam/dnsproxy v0.78.0 h1:HkshrCPbnciJzl8LLkOmaO6J+aWNSbatggCx2xBf4B8= 2 | github.com/AdguardTeam/dnsproxy v0.78.0/go.mod h1:uoOWRv2/FkZGlstG0r/+zwc1C3ypMgzeaeOCv7MTWWI= 3 | github.com/AdguardTeam/golibs v0.35.2 h1:GVlx/CiCz5ZXQmyvFrE3JyeGsgubE8f4rJvRshYJVVs= 4 | github.com/AdguardTeam/golibs v0.35.2/go.mod h1:p/l6tG7QCv+Hi5yVpv1oZInoatRGOWoyD1m+Ume+ZNY= 5 | github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 6 | github.com/Workiva/go-datastructures v1.1.7 h1:q5RXlAeKm3zDpZTbYXwdMb1gN9RtGSvOCtPXGJJL6Cs= 7 | github.com/Workiva/go-datastructures v1.1.7/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A= 8 | github.com/ameshkov/dnscrypt/v2 v2.4.0 h1:if6ZG2cuQmcP2TwSY+D0+8+xbPfoatufGlOQTMNkI9o= 9 | github.com/ameshkov/dnscrypt/v2 v2.4.0/go.mod h1:WpEFV2uhebXb8Jhes/5/fSdpmhGV8TL22RDaeWwV6hI= 10 | github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo= 11 | github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A= 12 | github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 h1:0b2vaepXIfMsG++IsjHiI2p4bxALD1Y2nQKGMR5zDQM= 13 | github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA= 14 | github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= 15 | github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= 16 | github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= 17 | github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 18 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 19 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 21 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 23 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 24 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 25 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 26 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 27 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 28 | github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= 29 | github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= 30 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 31 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 32 | github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= 33 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 34 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 35 | github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= 36 | github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= 37 | github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE= 38 | github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= 39 | github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= 40 | github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= 41 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 42 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 43 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 44 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 45 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 46 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 47 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 48 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 49 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 50 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 51 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 52 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 53 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 54 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 55 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 56 | github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg= 57 | github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q= 58 | github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ= 59 | github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo= 60 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 61 | go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= 62 | go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= 63 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 64 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 65 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 66 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 67 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 68 | golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0= 69 | golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0= 70 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 71 | golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= 72 | golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= 73 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 74 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 75 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 76 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 77 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 78 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 79 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 80 | golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= 81 | golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 82 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 83 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 84 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 85 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 86 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 87 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 88 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 89 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 90 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 91 | golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 92 | golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 93 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 94 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 95 | golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 96 | golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= 97 | golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= 98 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 99 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 100 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 101 | gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= 102 | gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 103 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 104 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 105 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 106 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 107 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 108 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 109 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 110 | -------------------------------------------------------------------------------- /internal/cmd/config.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/AdguardTeam/dnsproxy/proxy" 9 | "github.com/AdguardTeam/golibs/osutil" 10 | "github.com/AdguardTeam/golibs/timeutil" 11 | "gopkg.in/yaml.v3" 12 | ) 13 | 14 | // Configuration represents dnsproxy Configuration. 15 | type Configuration struct { 16 | // ConfigPath is the path to the Configuration file. 17 | ConfigPath string 18 | 19 | // LogOutput is the path to the log file. 20 | LogOutput string `yaml:"output"` 21 | 22 | // TLSCertPath is the path to the .crt with the certificate chain. 23 | TLSCertPath string `yaml:"tls-crt"` 24 | 25 | // TLSKeyPath is the path to the file with the private key. 26 | TLSKeyPath string `yaml:"tls-key"` 27 | 28 | // HTTPSServerName sets Server header for the HTTPS server. 29 | HTTPSServerName string `yaml:"https-server-name"` 30 | 31 | // HTTPSUserinfo is the sole permitted userinfo for the DoH basic 32 | // authentication. If it is set, all DoH queries are required to have this 33 | // basic authentication information. 34 | HTTPSUserinfo string `yaml:"https-userinfo"` 35 | 36 | // DNSCryptConfigPath is the path to the DNSCrypt Configuration file. 37 | DNSCryptConfigPath string `yaml:"dnscrypt-config"` 38 | 39 | // EDNSAddr is the custom EDNS Client Address to send. 40 | EDNSAddr string `yaml:"edns-addr"` 41 | 42 | // UpstreamMode determines the logic through which upstreams will be used. 43 | // If not specified the [proxy.UpstreamModeLoadBalance] is used. 44 | UpstreamMode string `yaml:"upstream-mode"` 45 | 46 | // ListenAddrs is the list of server's listen addresses. 47 | ListenAddrs []string `yaml:"listen-addrs"` 48 | 49 | // ListenPorts are the ports server listens on. 50 | ListenPorts []int `yaml:"listen-ports"` 51 | 52 | // HTTPSListenPorts are the ports server listens on for DNS-over-HTTPS. 53 | HTTPSListenPorts []int `yaml:"https-port"` 54 | 55 | // TLSListenPorts are the ports server listens on for DNS-over-TLS. 56 | TLSListenPorts []int `yaml:"tls-port"` 57 | 58 | // QUICListenPorts are the ports server listens on for DNS-over-QUIC. 59 | QUICListenPorts []int `yaml:"quic-port"` 60 | 61 | // DNSCryptListenPorts are the ports server listens on for DNSCrypt. 62 | DNSCryptListenPorts []int `yaml:"dnscrypt-port"` 63 | 64 | // Upstreams is the list of DNS upstream servers. 65 | Upstreams []string `yaml:"upstream"` 66 | 67 | // BootstrapDNS is the list of bootstrap DNS upstream servers. 68 | BootstrapDNS []string `yaml:"bootstrap"` 69 | 70 | // Fallbacks is the list of fallback DNS upstream servers. 71 | Fallbacks []string `yaml:"fallback"` 72 | 73 | // PrivateRDNSUpstreams are upstreams to use for reverse DNS lookups of 74 | // private addresses, including the requests for authority records, such as 75 | // SOA and NS. 76 | PrivateRDNSUpstreams []string `yaml:"private-rdns-upstream"` 77 | 78 | // DNS64Prefix defines the DNS64 prefixes that dnsproxy should use when it 79 | // acts as a DNS64 server. If not specified, dnsproxy uses the default 80 | // Well-Known Prefix. This option can be specified multiple times. 81 | DNS64Prefix []string `yaml:"dns64-prefix"` 82 | 83 | // PrivateSubnets is the list of private subnets to determine private 84 | // addresses. 85 | PrivateSubnets []string `yaml:"private-subnets"` 86 | 87 | // BogusNXDomain transforms responses that contain at least one of the given 88 | // IP addresses into NXDOMAIN. 89 | // 90 | // TODO(a.garipov): Find a way to use [netutil.Prefix]. Currently, package 91 | // go-flags doesn't support text unmarshalers. 92 | BogusNXDomain []string `yaml:"bogus-nxdomain"` 93 | 94 | // HostsFiles is the list of paths to the hosts files to resolve from. 95 | HostsFiles []string `yaml:"hosts-files"` 96 | 97 | // Timeout for outbound DNS queries to remote upstream servers in a 98 | // human-readable form. Default is 10s. 99 | Timeout timeutil.Duration `yaml:"timeout"` 100 | 101 | // CacheMinTTL is the minimum TTL value for caching DNS entries, in seconds. 102 | // It overrides the TTL value from the upstream server, if the one is less. 103 | CacheMinTTL uint32 `yaml:"cache-min-ttl"` 104 | 105 | // CacheMaxTTL is the maximum TTL value for caching DNS entries, in seconds. 106 | // It overrides the TTL value from the upstream server, if the one is 107 | // greater. 108 | CacheMaxTTL uint32 `yaml:"cache-max-ttl"` 109 | 110 | // OptimisticAnswerTTL is the default TTL for expired cached responses 111 | // in seconds. 112 | OptimisticAnswerTTL timeutil.Duration `yaml:"optimistic-answer-ttl"` 113 | 114 | // OptimisticMaxAge is the maximum time entries remain in the cache 115 | // when cache is optimistic. 116 | OptimisticMaxAge timeutil.Duration `yaml:"optimistic-max-age"` 117 | 118 | // CacheSizeBytes is the cache size in bytes. Default is 64k. 119 | CacheSizeBytes int `yaml:"cache-size"` 120 | 121 | // Ratelimit is the maximum number of requests per second. 122 | Ratelimit int `yaml:"ratelimit"` 123 | 124 | // RatelimitSubnetLenIPv4 is a subnet length for IPv4 addresses used for 125 | // rate limiting requests. 126 | RatelimitSubnetLenIPv4 int `yaml:"ratelimit-subnet-len-ipv4"` 127 | 128 | // RatelimitSubnetLenIPv6 is a subnet length for IPv6 addresses used for 129 | // rate limiting requests. 130 | RatelimitSubnetLenIPv6 int `yaml:"ratelimit-subnet-len-ipv6"` 131 | 132 | // UDPBufferSize is the size of the UDP buffer in bytes. A value <= 0 will 133 | // use the system default. 134 | UDPBufferSize int `yaml:"udp-buf-size"` 135 | 136 | // MaxGoRoutines is the maximum number of goroutines. 137 | MaxGoRoutines uint `yaml:"max-go-routines"` 138 | 139 | // TLSMinVersion is the minimum allowed version of TLS. 140 | // 141 | // TODO(d.kolyshev): Use more suitable type. 142 | TLSMinVersion float32 `yaml:"tls-min-version"` 143 | 144 | // TLSMaxVersion is the maximum allowed version of TLS. 145 | // 146 | // TODO(d.kolyshev): Use more suitable type. 147 | TLSMaxVersion float32 `yaml:"tls-max-version"` 148 | 149 | // help, if true, prints the command-line option help message and quit with 150 | // a successful exit-code. 151 | help bool 152 | 153 | // HostsFileEnabled controls whether hosts files are used for resolving or 154 | // not. 155 | HostsFileEnabled bool `yaml:"hosts-file-enabled"` 156 | 157 | // Pprof defines whether the pprof information needs to be exposed via 158 | // localhost:6060 or not. 159 | Pprof bool `yaml:"pprof"` 160 | 161 | // Version, if true, prints the program version, and exits. 162 | Version bool `yaml:"version"` 163 | 164 | // Verbose controls the verbosity of the output. 165 | Verbose bool `yaml:"verbose"` 166 | 167 | // Insecure disables upstream servers TLS certificate verification. 168 | Insecure bool `yaml:"insecure"` 169 | 170 | // IPv6Disabled makes the server to respond with NODATA to all AAAA queries. 171 | IPv6Disabled bool `yaml:"ipv6-disabled"` 172 | 173 | // HTTP3 controls whether HTTP/3 is enabled for this instance of dnsproxy. 174 | // It enables HTTP/3 support for both the DoH upstreams and the DoH server. 175 | HTTP3 bool `yaml:"http3"` 176 | 177 | // CacheOptimistic, if set to true, enables the optimistic DNS cache. That 178 | // means that cached results will be served even if their cache TTL has 179 | // already expired. 180 | CacheOptimistic bool `yaml:"cache-optimistic"` 181 | 182 | // Cache controls whether DNS responses are cached or not. 183 | Cache bool `yaml:"cache"` 184 | 185 | // RefuseAny makes the server to refuse requests of type ANY. 186 | RefuseAny bool `yaml:"refuse-any"` 187 | 188 | // EnableEDNSSubnet uses EDNS Client Subnet extension. 189 | EnableEDNSSubnet bool `yaml:"edns"` 190 | 191 | // PendingRequestsEnabled controls whether the server should track duplicate 192 | // queries and only send the first of them to the upstream server. It is 193 | // used to mitigate the cache poisoning attacks. 194 | PendingRequestsEnabled bool `yaml:"pending-requests-enabled"` 195 | 196 | // DNS64 defines whether DNS64 functionality is enabled or not. 197 | DNS64 bool `yaml:"dns64"` 198 | 199 | // UsePrivateRDNS makes the server to use private upstreams for reverse DNS 200 | // lookups of private addresses, including the requests for authority 201 | // records, such as SOA and NS. 202 | UsePrivateRDNS bool `yaml:"use-private-rdns"` 203 | } 204 | 205 | // parseConfig returns options parsed from the command args or config file. If 206 | // no options have been parsed, it returns a suitable exit code and an error. 207 | func parseConfig() (conf *Configuration, exitCode int, err error) { 208 | conf = &Configuration{ 209 | HTTPSServerName: "dnsproxy", 210 | UpstreamMode: string(proxy.UpstreamModeLoadBalance), 211 | CacheSizeBytes: 64 * 1024, 212 | Timeout: timeutil.Duration(10 * time.Second), 213 | OptimisticAnswerTTL: timeutil.Duration(proxy.DefaultOptimisticAnswerTTL), 214 | OptimisticMaxAge: timeutil.Duration(proxy.DefaultOptimisticMaxAge), 215 | RatelimitSubnetLenIPv4: 24, 216 | RatelimitSubnetLenIPv6: 56, 217 | HostsFileEnabled: true, 218 | PendingRequestsEnabled: true, 219 | } 220 | 221 | err = parseCmdLineOptions(conf) 222 | exitCode, needExit := processCmdLineOptions(conf, err) 223 | if needExit { 224 | return nil, exitCode, err 225 | } 226 | 227 | confPath := conf.ConfigPath 228 | if confPath == "" { 229 | return conf, exitCode, nil 230 | } 231 | 232 | // TODO(d.kolyshev): Bootstrap and use slog. 233 | fmt.Printf("dnsproxy config path: %s\n", confPath) 234 | 235 | err = parseConfigFile(conf, confPath) 236 | if err != nil { 237 | return nil, osutil.ExitCodeFailure, fmt.Errorf( 238 | "parsing config file %s: %w", 239 | confPath, 240 | err, 241 | ) 242 | } 243 | 244 | // Parse command-line args again as it has priority over YAML config. 245 | err = parseCmdLineOptions(conf) 246 | if err != nil { 247 | // Don't wrap the error, because it's informative enough as is. 248 | return nil, osutil.ExitCodeFailure, err 249 | } 250 | 251 | return conf, exitCode, nil 252 | } 253 | 254 | // parseConfigFile fills options with the settings from file read by the given 255 | // path. 256 | func parseConfigFile(conf *Configuration, confPath string) (err error) { 257 | // #nosec G304 -- Trust the file path that is given in the args. 258 | b, err := os.ReadFile(confPath) 259 | if err != nil { 260 | return fmt.Errorf("reading file: %w", err) 261 | } 262 | 263 | err = yaml.Unmarshal(b, conf) 264 | if err != nil { 265 | return fmt.Errorf("unmarshalling file: %w", err) 266 | } 267 | 268 | return nil 269 | } 270 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "compress/gzip" 7 | "context" 8 | "fmt" 9 | "io" 10 | "log/slog" 11 | "net" 12 | "os" 13 | "regexp" 14 | "runtime" 15 | "strconv" 16 | "strings" 17 | "time" 18 | 19 | "github.com/AdguardTeam/dnsproxy/proxy" 20 | "github.com/AdguardTeam/golibs/logutil/slogutil" 21 | "github.com/AdguardTeam/golibs/netutil" 22 | "github.com/AdguardTeam/golibs/timeutil" 23 | "github.com/Workiva/go-datastructures/set" 24 | "github.com/honwen/aiodns/internal/cmd" 25 | "github.com/urfave/cli" 26 | "gopkg.in/yaml.v3" 27 | ) 28 | 29 | var ( 30 | ctx context.Context 31 | slogger *slog.Logger 32 | 33 | options = cmd.Configuration{ 34 | UpstreamMode: string(proxy.UpstreamModeParallel), 35 | EnableEDNSSubnet: true, 36 | TLSMinVersion: 1.2, 37 | } 38 | 39 | defaultUpstream = new(cli.StringSlice) 40 | specUpstream = new(cli.StringSlice) 41 | fallUpstream = new(cli.StringSlice) 42 | bootUpstream = new(cli.StringSlice) 43 | 44 | Version = "undefined" // nolint:gochecknoglobals 45 | ) 46 | 47 | func cliErrorExit(c *cli.Context, err error) { 48 | fmt.Printf("%+v", err) 49 | cli.ShowAppHelp(c) 50 | os.Exit(-1) 51 | } 52 | 53 | func fetch(uri string, resolvers []string) (dat []byte, err error) { 54 | // Fetch List (Online or Local) 55 | if strings.HasPrefix(uri, "http://") || strings.HasPrefix(uri, "https://") { 56 | slogger.InfoContext(ctx, fmt.Sprintf("fetching online list: [%s]", uri)) 57 | dat, err = curl(uri, resolvers, 5) 58 | } else { 59 | if strings.HasPrefix(uri, "~") { 60 | homedir, _ := os.UserHomeDir() 61 | uri = homedir + uri[1:] 62 | } 63 | if strings.HasPrefix(uri, "$HOME") { 64 | homedir, _ := os.UserHomeDir() 65 | uri = homedir + uri[5:] 66 | } 67 | slogger.InfoContext(ctx, fmt.Sprintf("fetching local list: [%s]", uri)) 68 | dat, err = os.ReadFile(uri) 69 | } 70 | 71 | // gunzip if needed 72 | if strings.HasSuffix(uri, ".gz") { 73 | if zReader, zErr := gzip.NewReader(bytes.NewReader(dat)); zErr == nil { 74 | dat, _ = io.ReadAll(zReader) 75 | } else { 76 | err = zErr 77 | } 78 | } 79 | return 80 | } 81 | 82 | func scanDoamins(dat []byte, filter func(string) bool) (domains *set.Set) { 83 | domains = set.New() 84 | scanner := bufio.NewScanner(bytes.NewReader(dat)) 85 | re := regexp.MustCompile(`^(server|ipset)=/[^\/]*/`) 86 | for scanner.Scan() { 87 | it := strings.TrimSpace(scanner.Text()) 88 | for strings.HasPrefix(it, "#") { 89 | continue 90 | } 91 | for strings.HasPrefix(it, ".") { 92 | it = it[1:] 93 | } 94 | for strings.HasSuffix(it, ".") && len(it) > 0 { 95 | it = it[:len(it)-1] 96 | } 97 | if match := re.MatchString(it); match { 98 | it = it[8:strings.LastIndex(it, `/`)] 99 | } 100 | if len(it) <= 0 || (filter != nil && filter(it)) { 101 | continue 102 | } 103 | if netutil.ValidateDomainName(it) != nil { 104 | fmt.Printf("Domain Skiped: %s\n", it) 105 | continue 106 | } 107 | domains.Add(it) 108 | } 109 | return 110 | } 111 | 112 | func init() { 113 | ctx = context.Background() 114 | 115 | slogger = slogutil.New(&slogutil.Config{ 116 | Output: os.Stdout, 117 | Format: slogutil.FormatDefault, 118 | AddTimestamp: true, 119 | }) 120 | } 121 | 122 | func main() { 123 | app := cli.NewApp() 124 | app.Name = "AIO DNS" 125 | app.Usage = "All In One Clean DNS Solution." 126 | app.Version = fmt.Sprintf("Git:[%s](build in-data: %s)(dnsproxy: %s)(%s)", Version, embedDate, cmd.Version, runtime.Version()) 127 | 128 | app.Flags = []cli.Flag{ 129 | cli.StringFlag{ 130 | Name: "listen, l", 131 | Value: ":5300", 132 | Usage: "Listening address", 133 | }, 134 | cli.StringSliceFlag{ 135 | Name: "upstream, u", 136 | Value: defaultUpstream, 137 | Usage: "An upstream to be default used (can be specified multiple times)", 138 | }, 139 | cli.StringSliceFlag{ 140 | Name: "special-upstream, U", 141 | Value: specUpstream, 142 | Usage: "An upstream to be special used (can be specified multiple times)", 143 | }, 144 | cli.StringSliceFlag{ 145 | Name: "fallback, f", 146 | Value: fallUpstream, 147 | Usage: "Fallback resolvers to use when regular ones are unavailable, can be specified multiple times", 148 | }, 149 | cli.StringSliceFlag{ 150 | Name: "bootstrap, b", 151 | Value: bootUpstream, 152 | Usage: "Bootstrap DNS for DoH and DoT, can be specified multiple times", 153 | }, 154 | cli.StringSliceFlag{ 155 | Name: "special-list, L", 156 | Usage: "List of domains using special-upstream (can be specified multiple times), (file path from local or net)", 157 | }, 158 | cli.StringSliceFlag{ 159 | Name: "bypass-list, B", 160 | Usage: "List of domains bypass special-upstream (can be specified multiple times), (file path from local or net)", 161 | }, 162 | cli.StringFlag{ 163 | Name: "edns, e", 164 | Usage: "Send EDNS Client Address to default upstreams", 165 | }, 166 | cli.IntFlag{ 167 | Name: "timeout, t", 168 | Value: 1, 169 | Usage: "Timeout of Each upstream, [1, 59] seconds", 170 | }, 171 | cli.BoolFlag{ 172 | Name: "cache, C", 173 | Usage: "If specified, DNS cache is enabled", 174 | }, 175 | cli.BoolFlag{ 176 | Name: "insecure, I", 177 | Usage: "If specified, disable SSL/TLS Certificate check (for some OS without ca-certificates)", 178 | }, 179 | cli.BoolFlag{ 180 | Name: "ipv6-disabled, R", 181 | Usage: "If specified, all AAAA requests will be replied with NoError RCode and empty answer", 182 | }, 183 | cli.BoolFlag{ 184 | Name: "refuse-any, A", 185 | Usage: "If specified, refuse ANY requests", 186 | }, 187 | cli.BoolFlag{ 188 | Name: "fastest-addr, F", 189 | Usage: "If specified, Respond to A or AAAA requests only with the fastest IP address", 190 | }, 191 | cli.BoolFlag{ 192 | Name: "http3, H", 193 | Usage: "If specified, Enable HTTP/3 support", 194 | }, 195 | cli.BoolFlag{ 196 | Name: "verbose, V", 197 | Usage: "If specified, Verbose output", 198 | }, 199 | } 200 | 201 | app.Action = func(c *cli.Context) error { 202 | if !strings.HasPrefix(cmd.Version, "undefined") { 203 | fmt.Fprintf(os.Stderr, "%s %s\n", strings.ToUpper(c.App.Name), c.App.Version) 204 | } 205 | 206 | if host, port, err := net.SplitHostPort(c.String("listen")); err != nil { 207 | cliErrorExit(c, err) 208 | } else { 209 | if hostIP := net.ParseIP(host); hostIP != nil { 210 | options.ListenAddrs = append(options.ListenAddrs, host) 211 | } else { 212 | options.ListenAddrs = append(options.ListenAddrs, "0.0.0.0") 213 | } 214 | if portInt, err := strconv.Atoi(port); err == nil { 215 | options.ListenPorts = append(options.ListenPorts, portInt) 216 | } else { 217 | cliErrorExit(c, err) 218 | } 219 | } 220 | 221 | if timeout := c.Int("timeout"); 0 < timeout && timeout < 60 { 222 | options.Timeout = timeutil.Duration(time.Duration(timeout) * time.Second) 223 | } 224 | 225 | options.EDNSAddr = c.String("edns") 226 | options.Cache = c.BoolT("cache") 227 | options.Verbose = c.BoolT("verbose") 228 | options.Insecure = c.BoolT("insecure") 229 | options.RefuseAny = c.BoolT("refuse-any") 230 | options.IPv6Disabled = c.BoolT("ipv6-disabled") 231 | if c.BoolT("fastest-addr") { 232 | options.UpstreamMode = string(proxy.UpstreamModeFastestAddr) 233 | options.Cache = true 234 | options.CacheMinTTL = 600 235 | } 236 | if options.Cache { 237 | options.CacheSizeBytes = 4 * 1024 * 1024 // 4M 238 | options.CacheOptimistic = true // Prefetch 239 | } 240 | 241 | options.Upstreams = c.StringSlice("upstream") 242 | options.Fallbacks = c.StringSlice("fallback") 243 | options.BootstrapDNS = c.StringSlice("bootstrap") 244 | 245 | specLists := []string{} // list[domains mulit-lines] 246 | if len(c.StringSlice("special-list")) > 0 { 247 | for _, it := range c.StringSlice("special-list") { 248 | dat, err := fetch(it, options.BootstrapDNS) 249 | // skip if error 250 | if err != nil { 251 | slogger.InfoContext(ctx, fmt.Sprintf("%+v", err)) 252 | slogger.InfoContext(ctx, fmt.Sprintf("failed; skipped! [%s]", it)) 253 | continue 254 | } 255 | 256 | // append special list 257 | specLists = append(specLists, string(dat)) 258 | slogger.InfoContext(ctx, fmt.Sprintf("%d lines special list fetched", len(strings.Split(string(dat), "\n")))) 259 | } 260 | } 261 | 262 | // FailSafe or Default 263 | if len(specLists) <= 0 { 264 | slogger.InfoContext(ctx, "using build in special list") 265 | specLists = append(specLists, specList) 266 | specLists = append(specLists, tldnList) 267 | 268 | if os.Getenv("TIDY_UP") != "" { 269 | tldn := scanDoamins([]byte(tldnList), nil) 270 | tideSpec := scanDoamins([]byte(specList), func(s string) bool { 271 | for _, it := range tldn.Flatten() { 272 | if strings.HasSuffix(s, "."+it.(string)) { 273 | return true 274 | } 275 | } 276 | return false 277 | }) 278 | for _, it := range tideSpec.Flatten() { 279 | fmt.Println("#tideSpec", it) 280 | } 281 | 282 | tideBypass := scanDoamins([]byte(bypassList), func(s string) bool { 283 | for _, it := range tldn.Flatten() { 284 | if strings.HasSuffix(s, "."+it.(string)) { 285 | return false 286 | } 287 | } 288 | return true 289 | }) 290 | for _, it := range tideBypass.Flatten() { 291 | fmt.Println("#tideBypass", it) 292 | } 293 | os.Exit(0) 294 | } 295 | } 296 | 297 | specDomains := scanDoamins([]byte(strings.Join(specLists, "\n")), nil) 298 | 299 | for _, u := range c.StringSlice("special-upstream") { 300 | for _, it := range specDomains.Flatten() { 301 | nUpstream := fmt.Sprintf("[/%s/]%s", it, u) 302 | options.Upstreams = append(options.Upstreams, nUpstream) 303 | } 304 | } 305 | 306 | bypassDomains := set.New() 307 | if len(c.StringSlice("bypass-list")) > 0 { 308 | for _, it := range c.StringSlice("bypass-list") { 309 | dat, err := fetch(it, options.BootstrapDNS) 310 | // skip if error 311 | if err != nil { 312 | slogger.InfoContext(ctx, fmt.Sprintf("%+v", err)) 313 | slogger.InfoContext(ctx, fmt.Sprintf("failed; skipped! [%s]", it)) 314 | continue 315 | } 316 | 317 | // append bypass list 318 | bypassDomains.Add(scanDoamins(dat, nil).Flatten()...) 319 | slogger.InfoContext(ctx, fmt.Sprintf("%d lines bypass list fetched", len(strings.Split(string(dat), "\n")))) 320 | } 321 | } else if len(c.StringSlice("special list")) < 1 { 322 | // only use build in bypassList if special list NOT configured 323 | slogger.InfoContext(ctx, "using build in bypass list") 324 | bypassDomains = scanDoamins([]byte(bypassList), nil) 325 | } 326 | 327 | for _, it := range bypassDomains.Flatten() { 328 | nUpstream := fmt.Sprintf("[/%s/]%s", it, `#`) 329 | options.Upstreams = append(options.Upstreams, nUpstream) 330 | } 331 | 332 | for _, u := range initSpecUpstreams { 333 | for _, it := range initSpecDomains.Flatten() { 334 | // slogger.InfoContext(ctx, fmt.Sprintf("[/%s/]%s", it, u)) 335 | options.Upstreams = append(options.Upstreams, fmt.Sprintf("[/%s/]%s", it, u)) 336 | } 337 | } 338 | 339 | if options.Verbose { 340 | dump, _ := yaml.Marshal(&options) 341 | fmt.Println(string(dump)) 342 | } else { 343 | slogger.InfoContext(ctx, fmt.Sprintf("spec list length: %d", specDomains.Len())) 344 | slogger.InfoContext(ctx, fmt.Sprintf("bypass list length: %d", bypassDomains.Len())) 345 | slogger.InfoContext(ctx, fmt.Sprintf("upstream rules count: %d", len(options.Upstreams))) 346 | } 347 | 348 | err := cmd.RunProxy(ctx, slogger, &options) 349 | if err != nil { 350 | slogger.ErrorContext(ctx, "running dnsproxy", slogutil.KeyError, err) 351 | } 352 | return nil 353 | } 354 | app.Run(os.Args) 355 | } 356 | -------------------------------------------------------------------------------- /internal/cmd/proxy.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "log/slog" 8 | "net" 9 | "net/netip" 10 | "net/url" 11 | "os" 12 | "strings" 13 | "time" 14 | 15 | "github.com/AdguardTeam/dnsproxy/proxy" 16 | "github.com/AdguardTeam/dnsproxy/upstream" 17 | "github.com/AdguardTeam/golibs/errors" 18 | "github.com/AdguardTeam/golibs/logutil/slogutil" 19 | "github.com/AdguardTeam/golibs/netutil" 20 | "github.com/AdguardTeam/golibs/osutil" 21 | "github.com/ameshkov/dnscrypt/v2" 22 | 23 | "gopkg.in/yaml.v3" 24 | 25 | // TODO(internal): Move to AdguardTeam/golibs. 26 | "github.com/honwen/aiodns/internal/dnsmsg" 27 | "github.com/honwen/aiodns/internal/handler" 28 | proxynetutil "github.com/honwen/aiodns/internal/netutil" 29 | ) 30 | 31 | // TODO(e.burkov): Use a separate type for the YAML Configuration file. 32 | 33 | // createProxyConfig initializes [proxy.Config]. l must not be nil. 34 | func createProxyConfig( 35 | ctx context.Context, 36 | l *slog.Logger, 37 | conf *Configuration, 38 | ) (proxyConf *proxy.Config, err error) { 39 | hostsFiles, err := conf.hostsFiles(ctx, l) 40 | if err != nil { 41 | // Don't wrap the error since it's informative enough as is. 42 | return nil, err 43 | } 44 | 45 | hosts, err := handler.ReadHosts(ctx, l, hostsFiles) 46 | if err != nil { 47 | return nil, fmt.Errorf("reading hosts files: %w", err) 48 | } 49 | 50 | reqHdlr := handler.NewDefault(&handler.DefaultConfig{ 51 | Logger: l.With(slogutil.KeyPrefix, "default_handler"), 52 | // TODO(e.burkov): Use the configured message constructor. 53 | MessageConstructor: dnsmsg.DefaultMessageConstructor{}, 54 | HaltIPv6: conf.IPv6Disabled, 55 | HostsFiles: hosts, 56 | }) 57 | 58 | proxyConf = &proxy.Config{ 59 | Logger: l.With(slogutil.KeyPrefix, proxy.LogPrefix), 60 | 61 | RatelimitSubnetLenIPv4: conf.RatelimitSubnetLenIPv4, 62 | RatelimitSubnetLenIPv6: conf.RatelimitSubnetLenIPv6, 63 | 64 | Ratelimit: conf.Ratelimit, 65 | CacheEnabled: conf.Cache, 66 | CacheSizeBytes: conf.CacheSizeBytes, 67 | CacheMinTTL: conf.CacheMinTTL, 68 | CacheMaxTTL: conf.CacheMaxTTL, 69 | CacheOptimisticAnswerTTL: time.Duration(conf.OptimisticAnswerTTL), 70 | CacheOptimisticMaxAge: time.Duration(conf.OptimisticMaxAge), 71 | CacheOptimistic: conf.CacheOptimistic, 72 | RefuseAny: conf.RefuseAny, 73 | HTTP3: conf.HTTP3, 74 | // TODO(e.burkov): The following CIDRs are aimed to match any address. 75 | // This is not quite proper approach to be used by default so think 76 | // about configuring it. 77 | TrustedProxies: netutil.SliceSubnetSet{ 78 | netip.MustParsePrefix("0.0.0.0/0"), 79 | netip.MustParsePrefix("::0/0"), 80 | }, 81 | EnableEDNSClientSubnet: conf.EnableEDNSSubnet, 82 | UDPBufferSize: conf.UDPBufferSize, 83 | HTTPSServerName: conf.HTTPSServerName, 84 | MaxGoroutines: conf.MaxGoRoutines, 85 | UsePrivateRDNS: conf.UsePrivateRDNS, 86 | PrivateSubnets: netutil.SubnetSetFunc(netutil.IsLocallyServed), 87 | RequestHandler: reqHdlr.HandleRequest, 88 | PendingRequests: &proxy.PendingRequestsConfig{ 89 | Enabled: conf.PendingRequestsEnabled, 90 | }, 91 | } 92 | 93 | if uiStr := conf.HTTPSUserinfo; uiStr != "" { 94 | user, pass, ok := strings.Cut(uiStr, ":") 95 | if ok { 96 | proxyConf.Userinfo = url.UserPassword(user, pass) 97 | } else { 98 | proxyConf.Userinfo = url.User(user) 99 | } 100 | } 101 | 102 | conf.initBogusNXDomain(ctx, l, proxyConf) 103 | 104 | var errs []error 105 | errs = append(errs, conf.initUpstreams(ctx, l, proxyConf)) 106 | errs = append(errs, conf.initEDNS(ctx, l, proxyConf)) 107 | errs = append(errs, conf.initTLSConfig(proxyConf)) 108 | errs = append(errs, conf.initDNSCryptConfig(proxyConf)) 109 | errs = append(errs, conf.initListenAddrs(proxyConf)) 110 | errs = append(errs, conf.initSubnets(proxyConf)) 111 | 112 | return proxyConf, errors.Join(errs...) 113 | } 114 | 115 | // isEmpty returns false if uc contains at least a single upstream. uc must not 116 | // be nil. 117 | // 118 | // TODO(e.burkov): Think of a better way to validate the config. Perhaps, 119 | // return an error from [ParseUpstreamsConfig] if no upstreams were initialized. 120 | func isEmpty(uc *proxy.UpstreamConfig) (ok bool) { 121 | return len(uc.Upstreams) == 0 && 122 | len(uc.DomainReservedUpstreams) == 0 && 123 | len(uc.SpecifiedDomainUpstreams) == 0 124 | } 125 | 126 | // defaultLocalTimeout is the default timeout for local operations. 127 | const defaultLocalTimeout = 1 * time.Second 128 | 129 | // initUpstreams inits upstream-related config fields. 130 | // 131 | // TODO(d.kolyshev): Join errors. 132 | func (conf *Configuration) initUpstreams( 133 | ctx context.Context, 134 | l *slog.Logger, 135 | config *proxy.Config, 136 | ) (err error) { 137 | httpVersions := upstream.DefaultHTTPVersions 138 | if conf.HTTP3 { 139 | httpVersions = []upstream.HTTPVersion{ 140 | upstream.HTTPVersion3, 141 | upstream.HTTPVersion2, 142 | upstream.HTTPVersion11, 143 | } 144 | } 145 | 146 | timeout := time.Duration(conf.Timeout) 147 | bootOpts := &upstream.Options{ 148 | Logger: l, 149 | HTTPVersions: httpVersions, 150 | InsecureSkipVerify: conf.Insecure, 151 | Timeout: timeout, 152 | } 153 | boot, err := initBootstrap(ctx, l, conf.BootstrapDNS, bootOpts) 154 | if err != nil { 155 | return fmt.Errorf("initializing bootstrap: %w", err) 156 | } 157 | 158 | upsOpts := &upstream.Options{ 159 | Logger: l, 160 | HTTPVersions: httpVersions, 161 | InsecureSkipVerify: conf.Insecure, 162 | Bootstrap: boot, 163 | Timeout: timeout, 164 | } 165 | upstreams := loadServersList(conf.Upstreams) 166 | 167 | config.UpstreamConfig, err = proxy.ParseUpstreamsConfig(upstreams, upsOpts) 168 | if err != nil { 169 | return fmt.Errorf("parsing upstreams Configuration: %w", err) 170 | } 171 | 172 | privateUpsOpts := &upstream.Options{ 173 | Logger: l, 174 | HTTPVersions: httpVersions, 175 | Bootstrap: boot, 176 | Timeout: min(defaultLocalTimeout, timeout), 177 | } 178 | privateUpstreams := loadServersList(conf.PrivateRDNSUpstreams) 179 | 180 | private, err := proxy.ParseUpstreamsConfig(privateUpstreams, privateUpsOpts) 181 | if err != nil { 182 | return fmt.Errorf("parsing private rdns upstreams Configuration: %w", err) 183 | } 184 | 185 | if !isEmpty(private) { 186 | config.PrivateRDNSUpstreamConfig = private 187 | } 188 | 189 | fallbackUpstreams := loadServersList(conf.Fallbacks) 190 | fallbacks, err := proxy.ParseUpstreamsConfig(fallbackUpstreams, upsOpts) 191 | if err != nil { 192 | return fmt.Errorf("parsing fallback upstreams Configuration: %w", err) 193 | } 194 | 195 | if !isEmpty(fallbacks) { 196 | config.Fallbacks = fallbacks 197 | } 198 | 199 | if conf.UpstreamMode != "" { 200 | err = config.UpstreamMode.UnmarshalText([]byte(conf.UpstreamMode)) 201 | if err != nil { 202 | return fmt.Errorf("parsing upstream mode: %w", err) 203 | } 204 | 205 | return nil 206 | } 207 | 208 | config.UpstreamMode = proxy.UpstreamModeLoadBalance 209 | 210 | return nil 211 | } 212 | 213 | // initBootstrap initializes the [upstream.Resolver] for bootstrapping upstream 214 | // servers. It returns the default resolver if no bootstraps were specified. 215 | // The returned resolver will also use system hosts files first. 216 | func initBootstrap( 217 | ctx context.Context, 218 | l *slog.Logger, 219 | bootstraps []string, 220 | opts *upstream.Options, 221 | ) (r upstream.Resolver, err error) { 222 | var resolvers []upstream.Resolver 223 | 224 | for i, b := range bootstraps { 225 | var ur *upstream.UpstreamResolver 226 | ur, err = upstream.NewUpstreamResolver(b, opts) 227 | if err != nil { 228 | return nil, fmt.Errorf("creating bootstrap resolver at index %d: %w", i, err) 229 | } 230 | 231 | resolvers = append(resolvers, upstream.NewCachingResolver(ur)) 232 | } 233 | 234 | switch len(resolvers) { 235 | case 0: 236 | etcHosts, hostsErr := upstream.NewDefaultHostsResolver(ctx, osutil.RootDirFS(), l) 237 | if hostsErr != nil { 238 | l.ErrorContext(ctx, "creating default hosts resolver", slogutil.KeyError, hostsErr) 239 | 240 | return net.DefaultResolver, nil 241 | } 242 | 243 | return upstream.ConsequentResolver{etcHosts, net.DefaultResolver}, nil 244 | case 1: 245 | return resolvers[0], nil 246 | default: 247 | return upstream.ParallelResolver(resolvers), nil 248 | } 249 | } 250 | 251 | // initEDNS inits EDNS-related config fields. 252 | func (conf *Configuration) initEDNS( 253 | ctx context.Context, 254 | l *slog.Logger, 255 | config *proxy.Config, 256 | ) (err error) { 257 | if conf.EDNSAddr == "" { 258 | return nil 259 | } 260 | 261 | if !conf.EnableEDNSSubnet { 262 | l.WarnContext(ctx, "--edns is required", "--edns-addr", conf.EDNSAddr) 263 | 264 | return nil 265 | } 266 | 267 | config.EDNSAddr, err = netutil.ParseIP(conf.EDNSAddr) 268 | if err != nil { 269 | return fmt.Errorf("parsing edns-addr: %w", err) 270 | } 271 | 272 | return nil 273 | } 274 | 275 | // initBogusNXDomain inits BogusNXDomain structure. 276 | func (conf *Configuration) initBogusNXDomain( 277 | ctx context.Context, 278 | l *slog.Logger, 279 | config *proxy.Config, 280 | ) { 281 | if len(conf.BogusNXDomain) == 0 { 282 | return 283 | } 284 | 285 | for i, s := range conf.BogusNXDomain { 286 | p, err := proxynetutil.ParseSubnet(s) 287 | if err != nil { 288 | // TODO(a.garipov): Consider returning this err as a proper error. 289 | l.WarnContext(ctx, "parsing bogus nxdomain", "index", i, slogutil.KeyError, err) 290 | } else { 291 | config.BogusNXDomain = append(config.BogusNXDomain, p) 292 | } 293 | } 294 | } 295 | 296 | // initTLSConfig inits the TLS config. 297 | func (conf *Configuration) initTLSConfig(config *proxy.Config) (err error) { 298 | if conf.TLSCertPath != "" && conf.TLSKeyPath != "" { 299 | var tlsConfig *tls.Config 300 | tlsConfig, err = newTLSConfig(conf) 301 | if err != nil { 302 | return fmt.Errorf("loading TLS config: %w", err) 303 | } 304 | 305 | config.TLSConfig = tlsConfig 306 | } 307 | 308 | return nil 309 | } 310 | 311 | // initDNSCryptConfig inits the DNSCrypt config. 312 | func (conf *Configuration) initDNSCryptConfig(config *proxy.Config) (err error) { 313 | if conf.DNSCryptConfigPath == "" { 314 | return nil 315 | } 316 | 317 | b, err := os.ReadFile(conf.DNSCryptConfigPath) 318 | if err != nil { 319 | return fmt.Errorf("reading DNSCrypt config %q: %w", conf.DNSCryptConfigPath, err) 320 | } 321 | 322 | rc := &dnscrypt.ResolverConfig{} 323 | err = yaml.Unmarshal(b, rc) 324 | if err != nil { 325 | return fmt.Errorf("unmarshalling DNSCrypt config: %w", err) 326 | } 327 | 328 | cert, err := rc.CreateCert() 329 | if err != nil { 330 | return fmt.Errorf("creating DNSCrypt certificate: %w", err) 331 | } 332 | 333 | config.DNSCryptResolverCert = cert 334 | config.DNSCryptProviderName = rc.ProviderName 335 | 336 | return nil 337 | } 338 | 339 | // parseListenAddrs returns a slice of listen IP addresses from the given 340 | // options. In case no addresses are specified by options returns a slice with 341 | // the IPv4 unspecified address "0.0.0.0". 342 | // 343 | // TODO(d.kolyshev): Join errors. 344 | func parseListenAddrs(addrStrs []string) (addrs []netip.Addr, err error) { 345 | for i, a := range addrStrs { 346 | var ip netip.Addr 347 | ip, err = netip.ParseAddr(a) 348 | if err != nil { 349 | return addrs, fmt.Errorf("parsing listen address at index %d: %s", i, a) 350 | } 351 | 352 | addrs = append(addrs, ip) 353 | } 354 | 355 | if len(addrs) == 0 { 356 | // If ListenAddrs has not been parsed through config file nor command 357 | // line we set it to "0.0.0.0". 358 | // 359 | // TODO(a.garipov): Consider using localhost. 360 | addrs = append(addrs, netip.IPv4Unspecified()) 361 | } 362 | 363 | return addrs, nil 364 | } 365 | 366 | // initListenAddrs sets up proxy Configuration listen IP addresses. 367 | func (conf *Configuration) initListenAddrs(config *proxy.Config) (err error) { 368 | addrs, err := parseListenAddrs(conf.ListenAddrs) 369 | if err != nil { 370 | return fmt.Errorf("parsing listen addresses: %w", err) 371 | } 372 | 373 | if len(conf.ListenPorts) == 0 { 374 | // If ListenPorts has not been parsed through config file nor command 375 | // line we set it to 53. 376 | conf.ListenPorts = []int{53} 377 | } 378 | 379 | for _, port := range conf.ListenPorts { 380 | for _, ip := range addrs { 381 | addrPort := netip.AddrPortFrom(ip, uint16(port)) 382 | 383 | config.UDPListenAddr = append(config.UDPListenAddr, net.UDPAddrFromAddrPort(addrPort)) 384 | config.TCPListenAddr = append(config.TCPListenAddr, net.TCPAddrFromAddrPort(addrPort)) 385 | } 386 | } 387 | 388 | initTLSListenAddrs(config, conf, addrs) 389 | initDNSCryptListenAddrs(config, conf, addrs) 390 | 391 | return nil 392 | } 393 | 394 | // initTLSListenAddrs sets up proxy Configuration TLS listen addresses. 395 | func initTLSListenAddrs(proxyConf *proxy.Config, conf *Configuration, addrs []netip.Addr) { 396 | if proxyConf.TLSConfig == nil { 397 | return 398 | } 399 | 400 | for _, ip := range addrs { 401 | for _, port := range conf.TLSListenPorts { 402 | a := net.TCPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16(port))) 403 | proxyConf.TLSListenAddr = append(proxyConf.TLSListenAddr, a) 404 | } 405 | 406 | for _, port := range conf.HTTPSListenPorts { 407 | a := net.TCPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16(port))) 408 | proxyConf.HTTPSListenAddr = append(proxyConf.HTTPSListenAddr, a) 409 | } 410 | 411 | for _, port := range conf.QUICListenPorts { 412 | a := net.UDPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16(port))) 413 | proxyConf.QUICListenAddr = append(proxyConf.QUICListenAddr, a) 414 | } 415 | } 416 | } 417 | 418 | // initDNSCryptListenAddrs sets up proxy Configuration DNSCrypt listen 419 | // addresses. 420 | func initDNSCryptListenAddrs(proxyConf *proxy.Config, conf *Configuration, addrs []netip.Addr) { 421 | if proxyConf.DNSCryptResolverCert == nil || proxyConf.DNSCryptProviderName == "" { 422 | return 423 | } 424 | 425 | for _, port := range conf.DNSCryptListenPorts { 426 | p := uint16(port) 427 | 428 | for _, ip := range addrs { 429 | addrPort := netip.AddrPortFrom(ip, p) 430 | 431 | tcp := net.TCPAddrFromAddrPort(addrPort) 432 | proxyConf.DNSCryptTCPListenAddr = append(proxyConf.DNSCryptTCPListenAddr, tcp) 433 | 434 | udp := net.UDPAddrFromAddrPort(addrPort) 435 | proxyConf.DNSCryptUDPListenAddr = append(proxyConf.DNSCryptUDPListenAddr, udp) 436 | } 437 | } 438 | } 439 | 440 | // initSubnets sets the DNS64 Configuration into conf. 441 | // 442 | // TODO(d.kolyshev): Join errors. 443 | func (conf *Configuration) initSubnets(proxyConf *proxy.Config) (err error) { 444 | if proxyConf.UseDNS64 = conf.DNS64; proxyConf.UseDNS64 { 445 | for i, p := range conf.DNS64Prefix { 446 | var pref netip.Prefix 447 | pref, err = netip.ParsePrefix(p) 448 | if err != nil { 449 | return fmt.Errorf("parsing dns64 prefix at index %d: %w", i, err) 450 | } 451 | 452 | proxyConf.DNS64Prefs = append(proxyConf.DNS64Prefs, pref) 453 | } 454 | } 455 | 456 | if !conf.UsePrivateRDNS { 457 | return nil 458 | } 459 | 460 | return conf.initPrivateSubnets(proxyConf) 461 | } 462 | 463 | // initSubnets sets the private subnets Configuration into conf. 464 | func (conf *Configuration) initPrivateSubnets(proxyConf *proxy.Config) (err error) { 465 | private := make([]netip.Prefix, 0, len(conf.PrivateSubnets)) 466 | for i, p := range conf.PrivateSubnets { 467 | var pref netip.Prefix 468 | pref, err = netip.ParsePrefix(p) 469 | if err != nil { 470 | return fmt.Errorf("parsing private subnet at index %d: %w", i, err) 471 | } 472 | 473 | private = append(private, pref) 474 | } 475 | 476 | if len(private) > 0 { 477 | proxyConf.PrivateSubnets = netutil.SliceSubnetSet(private) 478 | } 479 | 480 | return nil 481 | } 482 | 483 | // loadServersList loads a list of DNS servers from the specified list. The 484 | // thing is that the user may specify either a server address or the path to a 485 | // file with a list of addresses. This method takes care of it, it reads the 486 | // file and loads servers from this file if needed. 487 | func loadServersList(sources []string) []string { 488 | var servers []string 489 | 490 | for _, source := range sources { 491 | // #nosec G304 -- Trust the file path that is given in the 492 | // Configuration. 493 | data, err := os.ReadFile(source) 494 | if err != nil { 495 | // Ignore errors, just consider it a server address and not a file. 496 | servers = append(servers, source) 497 | } 498 | 499 | lines := strings.Split(string(data), "\n") 500 | for _, line := range lines { 501 | line = strings.TrimSpace(line) 502 | 503 | // Ignore comments in the file. 504 | if line == "" || 505 | strings.HasPrefix(line, "!") || 506 | strings.HasPrefix(line, "#") { 507 | continue 508 | } 509 | 510 | servers = append(servers, line) 511 | } 512 | } 513 | 514 | return servers 515 | } 516 | 517 | // hostsFiles returns the list of hosts files to resolve from. It's empty if 518 | // resolving from hosts files is disabled. 519 | func (conf *Configuration) hostsFiles( 520 | ctx context.Context, 521 | l *slog.Logger, 522 | ) (paths []string, err error) { 523 | if !conf.HostsFileEnabled { 524 | l.DebugContext(ctx, "hosts files are disabled") 525 | 526 | return nil, nil 527 | } 528 | 529 | l.DebugContext(ctx, "hosts files are enabled") 530 | 531 | if len(conf.HostsFiles) > 0 { 532 | return conf.HostsFiles, nil 533 | } 534 | 535 | paths, err = proxynetutil.DefaultHostsPaths() 536 | if err != nil { 537 | return nil, fmt.Errorf("getting default hosts files: %w", err) 538 | } 539 | 540 | l.DebugContext(ctx, "hosts files are not specified, using default", "paths", paths) 541 | 542 | return paths, nil 543 | } 544 | -------------------------------------------------------------------------------- /internal/cmd/args.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "os" 8 | "slices" 9 | "strings" 10 | 11 | "github.com/AdguardTeam/golibs/errors" 12 | "github.com/AdguardTeam/golibs/osutil" 13 | "github.com/AdguardTeam/golibs/timeutil" 14 | ) 15 | 16 | // Indexes to help with the [commandLineOptions] initialization. 17 | const ( 18 | configPathIdx = iota 19 | logOutputIdx 20 | tlsCertPathIdx 21 | tlsKeyPathIdx 22 | httpsServerNameIdx 23 | httpsUserinfoIdx 24 | dnsCryptConfigPathIdx 25 | ednsAddrIdx 26 | upstreamModeIdx 27 | listenAddrsIdx 28 | listenPortsIdx 29 | httpsListenPortsIdx 30 | tlsListenPortsIdx 31 | quicListenPortsIdx 32 | dnsCryptListenPortsIdx 33 | upstreamsIdx 34 | bootstrapDNSIdx 35 | fallbacksIdx 36 | privateRDNSUpstreamsIdx 37 | dns64PrefixIdx 38 | privateSubnetsIdx 39 | bogusNXDomainIdx 40 | hostsFilesIdx 41 | timeoutIdx 42 | cacheMinTTLIdx 43 | cacheMaxTTLIdx 44 | cacheOptimisticAnswerTTLIdx 45 | cacheOptimisticMaxAgeIdx 46 | cacheSizeBytesIdx 47 | ratelimitIdx 48 | ratelimitSubnetLenIPv4Idx 49 | ratelimitSubnetLenIPv6Idx 50 | udpBufferSizeIdx 51 | maxGoRoutinesIdx 52 | tlsMinVersionIdx 53 | tlsMaxVersionIdx 54 | helpIdx 55 | hostsFileEnabledIdx 56 | pprofIdx 57 | versionIdx 58 | verboseIdx 59 | insecureIdx 60 | ipv6DisabledIdx 61 | http3Idx 62 | cacheOptimisticIdx 63 | cacheIdx 64 | refuseAnyIdx 65 | enableEDNSSubnetIdx 66 | pendingRequestsEnabledIdx 67 | dns64Idx 68 | usePrivateRDNSIdx 69 | ) 70 | 71 | // commandLineOption contains information about a command-line option: its long 72 | // and, if there is one, short forms, the value type, and the description. 73 | type commandLineOption struct { 74 | description string 75 | long string 76 | short string 77 | valueType string 78 | } 79 | 80 | // commandLineOptions are all command-line options currently supported by the 81 | // binary. 82 | var commandLineOptions = []*commandLineOption{ 83 | configPathIdx: { 84 | description: "YAML Configuration file. Minimal working Configuration in config.yaml.dist." + 85 | " Options passed through command line will override the ones from this file.", 86 | long: "config-path", 87 | short: "", 88 | valueType: "path", 89 | }, 90 | logOutputIdx: { 91 | description: `Path to the log file.`, 92 | long: "output", 93 | short: "o", 94 | valueType: "path", 95 | }, 96 | tlsCertPathIdx: { 97 | description: "Path to a file with the certificate chain.", 98 | long: "tls-crt", 99 | short: "c", 100 | valueType: "path", 101 | }, 102 | tlsKeyPathIdx: { 103 | description: "Path to a file with the private key.", 104 | long: "tls-key", 105 | short: "k", 106 | valueType: "path", 107 | }, 108 | httpsServerNameIdx: { 109 | description: "Set the Server header for the responses from the HTTPS server.", 110 | long: "https-server-name", 111 | short: "", 112 | valueType: "name", 113 | }, 114 | httpsUserinfoIdx: { 115 | description: "If set, all DoH queries are required to have this basic authentication " + 116 | "information.", 117 | long: "https-userinfo", 118 | short: "", 119 | valueType: "name", 120 | }, 121 | dnsCryptConfigPathIdx: { 122 | description: "Path to a file with DNSCrypt Configuration. You can generate one using " + 123 | "https://github.com/ameshkov/dnscrypt.", 124 | long: "dnscrypt-config", 125 | short: "g", 126 | valueType: "path", 127 | }, 128 | ednsAddrIdx: { 129 | description: "Send EDNS Client Address.", 130 | long: "edns-addr", 131 | short: "", 132 | valueType: "address", 133 | }, 134 | upstreamModeIdx: { 135 | description: "Defines the upstreams logic mode, possible values: load_balance, parallel, " + 136 | "fastest_addr (default: load_balance).", 137 | long: "upstream-mode", 138 | short: "", 139 | valueType: "mode", 140 | }, 141 | listenAddrsIdx: { 142 | description: "Listening addresses.", 143 | long: "listen", 144 | short: "l", 145 | valueType: "address", 146 | }, 147 | listenPortsIdx: { 148 | description: "Listening ports. Zero value disables TCP and UDP listeners.", 149 | long: "port", 150 | short: "p", 151 | valueType: "port", 152 | }, 153 | httpsListenPortsIdx: { 154 | description: "Listening ports for DNS-over-HTTPS.", 155 | long: "https-port", 156 | short: "s", 157 | valueType: "port", 158 | }, 159 | tlsListenPortsIdx: { 160 | description: "Listening ports for DNS-over-TLS.", 161 | long: "tls-port", 162 | short: "t", 163 | valueType: "port", 164 | }, 165 | quicListenPortsIdx: { 166 | description: "Listening ports for DNS-over-QUIC.", 167 | long: "quic-port", 168 | short: "q", 169 | valueType: "port", 170 | }, 171 | dnsCryptListenPortsIdx: { 172 | description: "Listening ports for DNSCrypt.", 173 | long: "dnscrypt-port", 174 | short: "y", 175 | valueType: "port", 176 | }, 177 | upstreamsIdx: { 178 | description: "An upstream to be used (can be specified multiple times). You can also " + 179 | "specify path to a file with the list of servers.", 180 | long: "upstream", 181 | short: "u", 182 | valueType: "", 183 | }, 184 | bootstrapDNSIdx: { 185 | description: "Bootstrap DNS for DoH and DoT, can be specified multiple times (default: " + 186 | "use system-provided).", 187 | long: "bootstrap", 188 | short: "b", 189 | valueType: "", 190 | }, 191 | fallbacksIdx: { 192 | description: "Fallback resolvers to use when regular ones are unavailable, can be " + 193 | "specified multiple times. You can also specify path to a file with the list of servers.", 194 | long: "fallback", 195 | short: "f", 196 | valueType: "", 197 | }, 198 | privateRDNSUpstreamsIdx: { 199 | description: "Private DNS upstreams to use for reverse DNS lookups of private addresses, " + 200 | "can be specified multiple times.", 201 | long: "private-rdns-upstream", 202 | short: "", 203 | valueType: "", 204 | }, 205 | dns64PrefixIdx: { 206 | description: "Prefix used to handle DNS64. If not specified, dnsproxy uses the " + 207 | "'Well-Known Prefix' 64:ff9b::. Can be specified multiple times.", 208 | long: "dns64-prefix", 209 | short: "", 210 | valueType: "subnet", 211 | }, 212 | privateSubnetsIdx: { 213 | description: "Private subnets to use for reverse DNS lookups of private addresses.", 214 | long: "private-subnets", 215 | short: "", 216 | valueType: "subnet", 217 | }, 218 | bogusNXDomainIdx: { 219 | description: "Transform the responses containing at least a single IP that matches " + 220 | "specified addresses and CIDRs into NXDOMAIN. Can be specified multiple times.", 221 | long: "bogus-nxdomain", 222 | short: "", 223 | valueType: "subnet", 224 | }, 225 | hostsFilesIdx: { 226 | description: "List of paths to the hosts files, can be specified multiple times.", 227 | long: "hosts-files", 228 | short: "", 229 | valueType: "path", 230 | }, 231 | timeoutIdx: { 232 | description: "Timeout for outbound DNS queries to remote upstream servers in a " + 233 | "human-readable form", 234 | long: "timeout", 235 | short: "", 236 | valueType: "duration", 237 | }, 238 | cacheMinTTLIdx: { 239 | description: "Minimum TTL value for DNS entries, in seconds. Capped at 3600. " + 240 | "Artificially extending TTLs should only be done with careful consideration.", 241 | long: "cache-min-ttl", 242 | short: "", 243 | valueType: "uint32", 244 | }, 245 | cacheMaxTTLIdx: { 246 | description: "Maximum TTL value for DNS entries, in seconds.", 247 | long: "cache-max-ttl", 248 | short: "", 249 | valueType: "uint32", 250 | }, 251 | cacheOptimisticAnswerTTLIdx: { 252 | description: "Default TTL value for expired answers from optimistic cache", 253 | long: "optimistic-answer-ttl", 254 | short: "", 255 | valueType: "duration", 256 | }, 257 | cacheOptimisticMaxAgeIdx: { 258 | description: "Period of time after which entries are removed from the optimistic cache", 259 | long: "optimistic-max-age", 260 | short: "", 261 | valueType: "duration", 262 | }, 263 | cacheSizeBytesIdx: { 264 | description: "Cache size (in bytes). Default: 64k.", 265 | long: "cache-size", 266 | short: "", 267 | valueType: "int", 268 | }, 269 | ratelimitIdx: { 270 | description: "Ratelimit (requests per second).", 271 | long: "ratelimit", 272 | short: "r", 273 | valueType: "int", 274 | }, 275 | ratelimitSubnetLenIPv4Idx: { 276 | description: "Ratelimit subnet length for IPv4.", 277 | long: "ratelimit-subnet-len-ipv4", 278 | short: "", 279 | valueType: "int", 280 | }, 281 | ratelimitSubnetLenIPv6Idx: { 282 | description: "Ratelimit subnet length for IPv6.", 283 | long: "ratelimit-subnet-len-ipv6", 284 | short: "", 285 | valueType: "int", 286 | }, 287 | udpBufferSizeIdx: { 288 | description: "Set the size of the UDP buffer in bytes. A value <= 0 will use the system " + 289 | "default.", 290 | long: "udp-buf-size", 291 | short: "", 292 | valueType: "int", 293 | }, 294 | maxGoRoutinesIdx: { 295 | description: "Set the maximum number of go routines. A zero value will not not set a " + 296 | "maximum.", 297 | long: "max-go-routines", 298 | short: "", 299 | valueType: "uint", 300 | }, 301 | tlsMinVersionIdx: { 302 | description: "Minimum TLS version, for example 1.0.", 303 | long: "tls-min-version", 304 | short: "", 305 | valueType: "version", 306 | }, 307 | tlsMaxVersionIdx: { 308 | description: "Maximum TLS version, for example 1.3.", 309 | long: "tls-max-version", 310 | short: "", 311 | valueType: "version", 312 | }, 313 | helpIdx: { 314 | description: "Print this help message and quit.", 315 | long: "help", 316 | short: "h", 317 | valueType: "", 318 | }, 319 | hostsFileEnabledIdx: { 320 | description: "If specified, use hosts files for resolving.", 321 | long: "hosts-file-enabled", 322 | short: "", 323 | valueType: "", 324 | }, 325 | pprofIdx: { 326 | description: "If present, exposes pprof information on localhost:6060.", 327 | long: "pprof", 328 | short: "", 329 | valueType: "", 330 | }, 331 | versionIdx: { 332 | description: "Prints the program version.", 333 | long: "version", 334 | short: "", 335 | valueType: "", 336 | }, 337 | verboseIdx: { 338 | description: "Verbose output.", 339 | long: "verbose", 340 | short: "v", 341 | valueType: "", 342 | }, 343 | insecureIdx: { 344 | description: "Disable secure TLS certificate validation.", 345 | long: "insecure", 346 | short: "", 347 | valueType: "", 348 | }, 349 | ipv6DisabledIdx: { 350 | description: "If specified, all AAAA requests will be replied with NoError RCode and " + 351 | "empty answer.", 352 | long: "ipv6-disabled", 353 | short: "", 354 | valueType: "", 355 | }, 356 | http3Idx: { 357 | description: "Enable HTTP/3 support.", 358 | long: "http3", 359 | short: "", 360 | valueType: "", 361 | }, 362 | cacheOptimisticIdx: { 363 | description: "If specified, optimistic DNS cache is enabled.", 364 | long: "cache-optimistic", 365 | short: "", 366 | valueType: "", 367 | }, 368 | cacheIdx: { 369 | description: "If specified, DNS cache is enabled.", 370 | long: "cache", 371 | short: "", 372 | valueType: "", 373 | }, 374 | refuseAnyIdx: { 375 | description: "If specified, refuses ANY requests.", 376 | long: "refuse-any", 377 | short: "", 378 | valueType: "", 379 | }, 380 | enableEDNSSubnetIdx: { 381 | description: "Use EDNS Client Subnet extension.", 382 | long: "edns", 383 | short: "", 384 | valueType: "", 385 | }, 386 | pendingRequestsEnabledIdx: { 387 | description: "If specified, the server will track duplicate queries and only send the " + 388 | "first of them to the upstream server, propagating its result to others. " + 389 | "Disabling it introduces a vulnerability to cache poisoning attacks.", 390 | long: "pending-requests-enabled", 391 | short: "", 392 | valueType: "", 393 | }, 394 | dns64Idx: { 395 | description: "If specified, dnsproxy will act as a DNS64 server.", 396 | long: "dns64", 397 | short: "", 398 | valueType: "", 399 | }, 400 | usePrivateRDNSIdx: { 401 | description: "If specified, use private upstreams for reverse DNS lookups of private " + 402 | "addresses.", 403 | long: "use-private-rdns", 404 | short: "", 405 | valueType: "", 406 | }, 407 | } 408 | 409 | // parseCmdLineOptions parses the command-line options. conf must not be nil. 410 | func parseCmdLineOptions(conf *Configuration) (err error) { 411 | cmdName, args := os.Args[0], os.Args[1:] 412 | 413 | flags := flag.NewFlagSet(cmdName, flag.ContinueOnError) 414 | for i, fieldPtr := range []any{ 415 | configPathIdx: &conf.ConfigPath, 416 | logOutputIdx: &conf.LogOutput, 417 | tlsCertPathIdx: &conf.TLSCertPath, 418 | tlsKeyPathIdx: &conf.TLSKeyPath, 419 | httpsServerNameIdx: &conf.HTTPSServerName, 420 | httpsUserinfoIdx: &conf.HTTPSUserinfo, 421 | dnsCryptConfigPathIdx: &conf.DNSCryptConfigPath, 422 | ednsAddrIdx: &conf.EDNSAddr, 423 | upstreamModeIdx: &conf.UpstreamMode, 424 | listenAddrsIdx: &conf.ListenAddrs, 425 | listenPortsIdx: &conf.ListenPorts, 426 | httpsListenPortsIdx: &conf.HTTPSListenPorts, 427 | tlsListenPortsIdx: &conf.TLSListenPorts, 428 | quicListenPortsIdx: &conf.QUICListenPorts, 429 | dnsCryptListenPortsIdx: &conf.DNSCryptListenPorts, 430 | upstreamsIdx: &conf.Upstreams, 431 | bootstrapDNSIdx: &conf.BootstrapDNS, 432 | fallbacksIdx: &conf.Fallbacks, 433 | privateRDNSUpstreamsIdx: &conf.PrivateRDNSUpstreams, 434 | dns64PrefixIdx: &conf.DNS64Prefix, 435 | privateSubnetsIdx: &conf.PrivateSubnets, 436 | bogusNXDomainIdx: &conf.BogusNXDomain, 437 | hostsFilesIdx: &conf.HostsFiles, 438 | timeoutIdx: &conf.Timeout, 439 | cacheMinTTLIdx: &conf.CacheMinTTL, 440 | cacheMaxTTLIdx: &conf.CacheMaxTTL, 441 | cacheOptimisticAnswerTTLIdx: &conf.OptimisticAnswerTTL, 442 | cacheOptimisticMaxAgeIdx: &conf.OptimisticMaxAge, 443 | cacheSizeBytesIdx: &conf.CacheSizeBytes, 444 | ratelimitIdx: &conf.Ratelimit, 445 | ratelimitSubnetLenIPv4Idx: &conf.RatelimitSubnetLenIPv4, 446 | ratelimitSubnetLenIPv6Idx: &conf.RatelimitSubnetLenIPv6, 447 | udpBufferSizeIdx: &conf.UDPBufferSize, 448 | maxGoRoutinesIdx: &conf.MaxGoRoutines, 449 | tlsMinVersionIdx: &conf.TLSMinVersion, 450 | tlsMaxVersionIdx: &conf.TLSMaxVersion, 451 | helpIdx: &conf.help, 452 | hostsFileEnabledIdx: &conf.HostsFileEnabled, 453 | pprofIdx: &conf.Pprof, 454 | versionIdx: &conf.Version, 455 | verboseIdx: &conf.Verbose, 456 | insecureIdx: &conf.Insecure, 457 | ipv6DisabledIdx: &conf.IPv6Disabled, 458 | http3Idx: &conf.HTTP3, 459 | cacheOptimisticIdx: &conf.CacheOptimistic, 460 | cacheIdx: &conf.Cache, 461 | refuseAnyIdx: &conf.RefuseAny, 462 | enableEDNSSubnetIdx: &conf.EnableEDNSSubnet, 463 | pendingRequestsEnabledIdx: &conf.PendingRequestsEnabled, 464 | dns64Idx: &conf.DNS64, 465 | usePrivateRDNSIdx: &conf.UsePrivateRDNS, 466 | } { 467 | addOption(flags, fieldPtr, commandLineOptions[i]) 468 | } 469 | 470 | flags.Usage = func() { usage(cmdName, os.Stderr) } 471 | 472 | err = flags.Parse(args) 473 | if err != nil { 474 | // Don't wrap the error, because it's informative enough as is. 475 | return err 476 | } 477 | 478 | nonFlags := flags.Args() 479 | if len(nonFlags) > 0 { 480 | return fmt.Errorf("positional arguments are not allowed, please check your command line "+ 481 | "arguments; detected positional arguments: %s", nonFlags) 482 | } 483 | 484 | return nil 485 | } 486 | 487 | // defineFlag defines a flag with specified setFlag function. o must not be 488 | // nil. 489 | func defineFlag[T any]( 490 | fieldPtr *T, 491 | o *commandLineOption, 492 | setFlag func(p *T, name string, value T, usage string), 493 | ) { 494 | setFlag(fieldPtr, o.long, *fieldPtr, o.description) 495 | if o.short != "" { 496 | setFlag(fieldPtr, o.short, *fieldPtr, o.description) 497 | } 498 | } 499 | 500 | // defineFlagVar defines a flag with the specified [flag.Value] value. o must 501 | // not be nil. 502 | func defineFlagVar(flags *flag.FlagSet, value flag.Value, o *commandLineOption) { 503 | flags.Var(value, o.long, o.description) 504 | if o.short != "" { 505 | flags.Var(value, o.short, o.description) 506 | } 507 | } 508 | 509 | // defineTimeutilDurationFlag defines a flag with for the specified 510 | // [*timeutil.Duration] pointer and command line option. o must not be nil. 511 | func defineTimeutilDurationFlag( 512 | flags *flag.FlagSet, 513 | fieldPtr *timeutil.Duration, 514 | o *commandLineOption, 515 | ) { 516 | flags.TextVar(fieldPtr, o.long, *fieldPtr, o.description) 517 | if o.short != "" { 518 | flags.TextVar(fieldPtr, o.short, *fieldPtr, o.description) 519 | } 520 | } 521 | 522 | // addOption adds the command-line option described by o to flags using fieldPtr 523 | // as the pointer to the value. 524 | func addOption(flags *flag.FlagSet, fieldPtr any, o *commandLineOption) { 525 | switch fieldPtr := fieldPtr.(type) { 526 | case *string: 527 | defineFlag(fieldPtr, o, flags.StringVar) 528 | case *bool: 529 | defineFlag(fieldPtr, o, flags.BoolVar) 530 | case *int: 531 | defineFlag(fieldPtr, o, flags.IntVar) 532 | case *uint: 533 | defineFlag(fieldPtr, o, flags.UintVar) 534 | case *uint32: 535 | defineFlagVar(flags, (*uint32Value)(fieldPtr), o) 536 | case *float32: 537 | defineFlagVar(flags, (*float32Value)(fieldPtr), o) 538 | case *[]int: 539 | defineFlagVar(flags, newIntSliceValue(fieldPtr), o) 540 | case *[]string: 541 | defineFlagVar(flags, newStringSliceValue(fieldPtr), o) 542 | case *timeutil.Duration: 543 | defineTimeutilDurationFlag(flags, fieldPtr, o) 544 | default: 545 | panic(fmt.Errorf("unexpected field pointer type %T: %w", fieldPtr, errors.ErrBadEnumValue)) 546 | } 547 | } 548 | 549 | // usage prints a usage message similar to the one printed by package flag but 550 | // taking long vs. short versions into account as well as using more informative 551 | // value hints. 552 | func usage(cmdName string, output io.Writer) { 553 | options := slices.Clone(commandLineOptions) 554 | slices.SortStableFunc(options, func(a, b *commandLineOption) (res int) { 555 | return strings.Compare(a.long, b.long) 556 | }) 557 | 558 | b := &strings.Builder{} 559 | _, _ = fmt.Fprintf(b, "Usage of %s:\n", cmdName) 560 | 561 | for _, o := range options { 562 | writeUsageLine(b, o) 563 | 564 | // Use four spaces before the tab to trigger good alignment for both 4- 565 | // and 8-space tab stops. 566 | _, _ = fmt.Fprintf(b, " \t%s\n", o.description) 567 | } 568 | 569 | _, _ = io.WriteString(output, b.String()) 570 | } 571 | 572 | // writeUsageLine writes the usage line for the provided command-line option. 573 | func writeUsageLine(b *strings.Builder, o *commandLineOption) { 574 | if o.short == "" { 575 | if o.valueType == "" { 576 | _, _ = fmt.Fprintf(b, " --%s\n", o.long) 577 | } else { 578 | _, _ = fmt.Fprintf(b, " --%s=%s\n", o.long, o.valueType) 579 | } 580 | 581 | return 582 | } 583 | 584 | if o.valueType == "" { 585 | _, _ = fmt.Fprintf(b, " --%s/-%s\n", o.long, o.short) 586 | } else { 587 | _, _ = fmt.Fprintf(b, " --%[1]s=%[3]s/-%[2]s %[3]s\n", o.long, o.short, o.valueType) 588 | } 589 | } 590 | 591 | // processCmdLineOptions decides if dnsproxy should exit depending on the 592 | // results of command-line option parsing. 593 | func processCmdLineOptions(conf *Configuration, parseErr error) (exitCode int, needExit bool) { 594 | if parseErr != nil { 595 | // Assume that usage has already been printed. 596 | return osutil.ExitCodeArgumentError, true 597 | } 598 | 599 | if conf.help { 600 | usage(os.Args[0], os.Stdout) 601 | 602 | return osutil.ExitCodeSuccess, true 603 | } 604 | 605 | if conf.Version { 606 | fmt.Printf("dnsproxy version %s\n", Version) 607 | 608 | return osutil.ExitCodeSuccess, true 609 | } 610 | 611 | return osutil.ExitCodeSuccess, false 612 | } 613 | --------------------------------------------------------------------------------