├── .gitignore ├── .github └── workflows │ └── build.yml ├── go.mod ├── go.sum ├── cmd ├── client │ └── main.go └── proxy │ └── main.go ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | # binaries 2 | /client 3 | /proxy 4 | 5 | # testing certs 6 | /cert.pem 7 | /key.pem 8 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | stable: true 20 | - name: build 21 | run: go build -v ./... 22 | - name: unit tests 23 | run: go test -v ./... 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ns1/doq-proxy 2 | 3 | go 1.23.3 4 | 5 | require ( 6 | github.com/go-kit/kit v0.13.0 7 | github.com/miekg/dns v1.1.62 8 | github.com/oklog/run v1.1.0 9 | github.com/quic-go/quic-go v0.48.1 10 | ) 11 | 12 | require ( 13 | github.com/go-kit/log v0.2.1 // indirect 14 | github.com/go-logfmt/logfmt v0.6.0 // indirect 15 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect 16 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect 17 | github.com/onsi/ginkgo/v2 v2.9.5 // indirect 18 | go.uber.org/mock v0.4.0 // indirect 19 | golang.org/x/crypto v0.26.0 // indirect 20 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect 21 | golang.org/x/mod v0.18.0 // indirect 22 | golang.org/x/net v0.28.0 // indirect 23 | golang.org/x/sync v0.8.0 // indirect 24 | golang.org/x/sys v0.23.0 // indirect 25 | golang.org/x/tools v0.22.0 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 2 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 3 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= 8 | github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= 9 | github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= 10 | github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= 11 | github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= 12 | github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 13 | github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= 14 | github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 15 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 16 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 17 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 18 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 19 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 20 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 21 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= 22 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 23 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 24 | github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= 25 | github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= 26 | github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= 27 | github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= 28 | github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= 29 | github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= 30 | github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= 31 | github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= 32 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 33 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 34 | github.com/quic-go/quic-go v0.48.1 h1:y/8xmfWI9qmGTc+lBr4jKRUWLGSlSigv847ULJ4hYXA= 35 | github.com/quic-go/quic-go v0.48.1/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= 36 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 37 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 38 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 39 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 40 | go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= 41 | go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= 42 | golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= 43 | golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 44 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= 45 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= 46 | golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= 47 | golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 48 | golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= 49 | golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 50 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 51 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 52 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 53 | golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= 54 | golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 55 | golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= 56 | golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 57 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 58 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 59 | golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= 60 | golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= 61 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 62 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 63 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 64 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 65 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 66 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 67 | -------------------------------------------------------------------------------- /cmd/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "encoding/binary" 8 | "errors" 9 | "flag" 10 | "fmt" 11 | "io" 12 | "os" 13 | "sync" 14 | "time" 15 | 16 | "github.com/miekg/dns" 17 | "github.com/quic-go/quic-go" 18 | ) 19 | 20 | type Query struct { 21 | Name string 22 | Type uint16 23 | } 24 | 25 | func main() { 26 | os.Exit(main2()) 27 | } 28 | 29 | func main2() int { 30 | var ( 31 | server string 32 | dnssec bool 33 | recursion bool 34 | keysPath string 35 | queries []Query 36 | timeout time.Duration 37 | tlsCA string 38 | tlsCert string 39 | tlsKey string 40 | ) 41 | 42 | flag.Usage = func() { 43 | fmt.Printf("usage: %s ( )...\n\n", os.Args[0]) 44 | flag.PrintDefaults() 45 | } 46 | 47 | flag.StringVar(&server, "server", "127.0.0.1:853", "DNS-over-QUIC server to use.") 48 | flag.BoolVar(&dnssec, "dnssec", true, "Send DNSSEC OK flag.") 49 | flag.BoolVar(&recursion, "recursion", true, "Send RD flag.") 50 | flag.StringVar(&keysPath, "export_keys_path", "", "File name to export session keys for decryption.") 51 | flag.DurationVar(&timeout, "timeout", 3*time.Second, "Connection timeout.") 52 | flag.StringVar(&tlsCA, "ca_certs", "", "Path to CA certificate bundle.") 53 | flag.StringVar(&tlsCert, "cert", "", "Path to client TLS certificate.") 54 | flag.StringVar(&tlsKey, "key", "", "Path to client TLS key.") 55 | flag.Parse() 56 | 57 | if flag.NArg() == 0 || flag.NArg()%2 != 0 { 58 | flag.Usage() 59 | return 1 60 | } 61 | 62 | for i := 0; (i + 1) < flag.NArg(); i += 2 { 63 | qname := dns.Fqdn(flag.Arg(i)) 64 | qtype, ok := dns.StringToType[flag.Arg(i+1)] 65 | if !ok { 66 | fmt.Fprintf(os.Stderr, "invalid qtype: %s\n", flag.Arg(i+1)) 67 | return 1 68 | } 69 | if qtype == dns.TypeIXFR { 70 | // TODO: Allow user to pass in serial number for IXFR 71 | fmt.Fprintf(os.Stderr, "skipping unsupported qtype: %s\n", flag.Arg(i+1)) 72 | } else { 73 | queries = append(queries, Query{qname, qtype}) 74 | } 75 | } 76 | 77 | var keyLog io.Writer 78 | if keysPath != "" { 79 | w, err := os.OpenFile(keysPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) 80 | if err != nil { 81 | fmt.Fprintf(os.Stderr, "failed to open file for session keys: %s\n", err) 82 | return 1 83 | } 84 | defer w.Close() 85 | keyLog = w 86 | } 87 | 88 | tlsConfig := tls.Config{ 89 | NextProtos: []string{"doq"}, 90 | KeyLogWriter: keyLog, 91 | } 92 | 93 | // server certificate validation 94 | if tlsCA == "" { 95 | tlsConfig.InsecureSkipVerify = true 96 | } else { 97 | data, err := os.ReadFile(tlsCA) 98 | if err != nil { 99 | fmt.Fprintf(os.Stderr, "failed to load CA certificate bundle: %s\n", err) 100 | return 1 101 | } 102 | pool := x509.NewCertPool() 103 | if ok := pool.AppendCertsFromPEM(data); !ok { 104 | fmt.Fprintf(os.Stderr, "failed to load CA certificate bundle: no certificate found\n") 105 | return 1 106 | } 107 | tlsConfig.RootCAs = pool 108 | } 109 | 110 | // client certificate 111 | if tlsCert != "" && tlsKey != "" { 112 | cert, err := tls.LoadX509KeyPair(tlsCert, tlsKey) 113 | if err != nil { 114 | fmt.Fprintf(os.Stderr, "failed to load TLS certificate: %s\n", err) 115 | return 1 116 | } 117 | 118 | tlsConfig.Certificates = append(tlsConfig.Certificates, cert) 119 | } 120 | 121 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 122 | session, err := quic.DialAddr(ctx, server, &tlsConfig, nil) 123 | cancel() 124 | if err != nil { 125 | fmt.Fprintf(os.Stderr, "failed to connect to the server: %s\n", err) 126 | return 1 127 | } 128 | defer session.CloseWithError(0, "") 129 | 130 | print := make(chan string) 131 | 132 | wg := sync.WaitGroup{} 133 | wg.Add(len(queries)) 134 | 135 | for _, query := range queries { 136 | go func(query Query) { 137 | err := SendQuery(session, &query, dnssec, recursion, print) 138 | if err != nil { 139 | print <- fmt.Sprintf("failed to send query: %s\n", err) 140 | } 141 | wg.Done() 142 | }(query) 143 | } 144 | 145 | go func() { 146 | wg.Wait() 147 | close(print) 148 | }() 149 | 150 | for p := range print { 151 | fmt.Println(p) 152 | } 153 | 154 | return 0 155 | } 156 | 157 | func SendQuery(session quic.Connection, query *Query, dnssec, recursion bool, print chan (string)) error { 158 | stream, err := session.OpenStream() 159 | if err != nil { 160 | return fmt.Errorf("open stream: %w", err) 161 | } 162 | 163 | msg := dns.Msg{} 164 | msg.SetQuestion(query.Name, query.Type) 165 | msg.RecursionDesired = recursion 166 | msg.SetEdns0(4096, dnssec) 167 | msg.Id = 0 168 | wire, err := msg.Pack() 169 | if err != nil { 170 | stream.Close() 171 | return fmt.Errorf("pack query: %w", err) 172 | } 173 | 174 | bundle := make([]byte, 2+len(wire)) 175 | binary.BigEndian.PutUint16(bundle, uint16(len(wire))) 176 | copy(bundle[2:], wire) 177 | _, err = stream.Write(bundle) 178 | stream.Close() 179 | if err != nil { 180 | return fmt.Errorf("send query: %w", err) 181 | } 182 | 183 | stream.SetDeadline(time.Now().Add(1 * time.Second)) 184 | 185 | for { 186 | var length uint16 187 | if err := binary.Read(stream, binary.BigEndian, &length); err != nil { 188 | // Ignore timeout related errors as this is how we close this connection for now 189 | if errors.Is(err, os.ErrDeadlineExceeded) { 190 | return nil 191 | } 192 | return fmt.Errorf("read response length: %w", err) 193 | } 194 | 195 | buf := make([]byte, length) 196 | _, err := io.ReadFull(stream, buf) 197 | if err != nil { 198 | return fmt.Errorf("read response payload: %w", err) 199 | } 200 | 201 | resp := dns.Msg{} 202 | err = resp.Unpack(buf) 203 | if err != nil { 204 | return fmt.Errorf("decode response: %w", err) 205 | } 206 | print <- resp.String() 207 | switch msg.Question[0].Qtype { 208 | case dns.TypeAXFR, dns.TypeIXFR: 209 | default: 210 | return nil 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DNS-over-QUIC to UDP Proxy 2 | 3 | DNS-over-QUIC to UDP proxy and client implementation. 4 | 5 | 2019—2023 © NS1 6 | 7 | ## License 8 | 9 | This code is released under Apache License 2.0. You can find terms and 10 | conditions in the LICENSE file. 11 | 12 | ## Protocol compatibility 13 | 14 | The implementation follows 15 | [RFC 9250: DNS over Dedicated QUIC Connections](https://www.rfc-editor.org/rfc/rfc9250.html). 16 | 17 | The QUIC protocol compatibility depends on the 18 | [quic-go](https:///github.com/quic-go/quic-go) library. 19 | 20 | ## Getting started 21 | 22 | Build the DoQ proxy and testing client. 23 | 24 | ``` 25 | go build ./cmd/proxy 26 | go build ./cmd/client 27 | ``` 28 | 29 | Generate testing key and self-signed certificate for the proxy server. 30 | 31 | ``` 32 | openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -pkeyopt ec_param_enc:named_curve -out server.key 33 | openssl req -x509 -days 30 -subj "/CN=DNS-over-QUIC Test" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1,IP:::1" -key server.key -out server.crt 34 | ``` 35 | 36 | Start the proxy. By default, the server loads the TLS key and certificate from 37 | the files generated above, will use 8.8.4.4 (Google Public DNS) as a backend 38 | server, and will listen on UDP port 853 (experimental port from the draft). Use 39 | command line options to modify the default behavior. Notice the use of the 40 | default port requires starting the proxy as superuser. 41 | 42 | ``` 43 | sudo ./proxy 44 | ``` 45 | 46 | Query the proxy using the testing utility. The client establishes a QUIC 47 | session to the server and sends each query via a dedicated stream. Upstream, the 48 | XFR requests are sent over TCP, all others are sent over UDP. The replies are 49 | printed in the order of completion: 50 | 51 | ``` 52 | ./client ns1.com A ns1.com AAAA 53 | ``` 54 | ``` 55 | ;; opcode: QUERY, status: NOERROR, id: 25849 56 | ;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1 57 | 58 | ;; QUESTION SECTION: 59 | ;ns1.com. IN AAAA 60 | 61 | ;; ANSWER SECTION: 62 | ns1.com. 195 IN AAAA 2606:4700:10::6814:31b6 63 | ns1.com. 195 IN AAAA 2606:4700:10::6814:30b6 64 | ns1.com. 195 IN RRSIG AAAA 13 2 200 20190325121641 20190323121641 44688 ns1.com. m17G7sGkXNhBiKINI2LuQLvUL0Qb+l6LMUmKSoVo2TP5sw3Yd27L44QOZhVU1GS//tD1e6YVOVsMrW3arlk/bQ== 65 | 66 | ;; ADDITIONAL SECTION: 67 | 68 | ;; OPT PSEUDOSECTION: 69 | ; EDNS: version 0; flags: do; udp: 512 70 | 71 | ;; opcode: QUERY, status: NOERROR, id: 26044 72 | ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1 73 | 74 | ;; QUESTION SECTION: 75 | ;ns1.com. IN A 76 | 77 | ;; ANSWER SECTION: 78 | ns1.com. 25 IN A 104.20.49.182 79 | ns1.com. 25 IN A 104.20.48.182 80 | ns1.com. 25 IN RRSIG A 13 2 26 20190325121645 20190323121645 44688 ns1.com. xJK5DhMiFqxWx/gC7gHQXM8wkVFDyocIF3Zuehqa+S92zAq3yOtZMrqVRXxsKNw2lfCMQXLHr7hVUDm5H4B5eA== 81 | 82 | ;; ADDITIONAL SECTION: 83 | 84 | ;; OPT PSEUDOSECTION: 85 | ; EDNS: version 0; flags: do; udp: 512 86 | ``` 87 | 88 | ## Client TLS certificate authentication (mTLS) 89 | 90 | The proxy server supports client certificate auhentication. In order to enable the feature, 91 | provide CA certificate bundle in PEM format as a proxy parameter `-mtls_ca_certs`. 92 | 93 | Generate a testing CA certificate: 94 | 95 | ``` 96 | openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -pkeyopt ec_param_enc:named_curve -out ca.key 97 | openssl req -x509 -days 30 -subj "/CN=DNS-over-QUIC Test CA" -addext "basicConstraints=critical,CA:true,pathlen:0" -key ca.key -out ca.crt 98 | ``` 99 | 100 | Generate server and client certificates and sign them with the CA certificate: 101 | 102 | ``` 103 | openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -pkeyopt ec_param_enc:named_curve -out server.key 104 | openssl req -x509 -CA ca.crt -CAkey ca.key -days 30 -subj "/CN=server" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1,IP:::1" -key server.key -out server.crt 105 | ``` 106 | 107 | ``` 108 | openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -pkeyopt ec_param_enc:named_curve -out client.key 109 | openssl req -x509 -CA ca.crt -CAkey ca.key -days 30 -subj "/CN=client" -key client.key -out client.crt 110 | ``` 111 | 112 | Run the proxy and enable mTLS by providing the CA certificate as a CA bundle: 113 | 114 | ``` 115 | ./proxy -mtls_ca_certs ca.crt 116 | ``` 117 | 118 | Run the query with providing client certificates. The CA bundle enables server certificate validation: 119 | 120 | ``` 121 | ./client -ca_certs ca.crt -cert client.crt -key client.key ns1.com. AAAA 122 | ``` 123 | 124 | ## Troubleshooting 125 | 126 | Note that this is an experimental code built on top of an experimental protocol. 127 | 128 | The server and client in this repository use the same QUIC library 129 | and therefore they should be compatible. However, if a different client is 130 | used, the handshake may fail on the version negotiation. We suggest to check 131 | packet capture first when the client is unable to connect. 132 | 133 | The proxy also logs information about accepted connections and streams which 134 | can be used to inspect the sequence of events: 135 | 136 | ``` 137 | $ sudo ./proxy -listen 127.0.0.1:853 -cert cert.pem -key key.pem -backend 8.8.4.4:53 138 | ts=2019-03-24T10:31:32.408891Z msg="listening for clients" addr=127.0.0.1:853 139 | ts=2019-03-24T12:16:45.048583Z client=127.0.0.1:52212 msg="session accepted" 140 | ts=2019-03-24T12:16:45.050231Z client=127.0.0.1:52212 stream_id=0 msg="stream accepted" 141 | ts=2019-03-24T12:16:45.050278Z client=127.0.0.1:52212 stream_id=4 msg="stream accepted" 142 | ts=2019-03-24T12:16:45.091568Z client=127.0.0.1:52212 stream_id=4 msg="stream closed" 143 | ts=2019-03-24T12:16:45.104623Z client=127.0.0.1:52212 stream_id=0 msg="stream closed" 144 | ts=2019-03-24T12:16:45.110261Z client=127.0.0.1:52212 msg="session closed" 145 | ``` 146 | 147 | ## Contributing 148 | 149 | This project is [maintained](https://github.com/ns1/community/blob/master/project_status/MAINTENANCE.md). 150 | 151 | Pull Requests and issues are welcome. See the [NS1 Contribution Guidelines](https://github.com/ns1/community) for more information. 152 | -------------------------------------------------------------------------------- /cmd/proxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "encoding/binary" 9 | "errors" 10 | "flag" 11 | "fmt" 12 | "io" 13 | "net" 14 | "os" 15 | "os/signal" 16 | "sync" 17 | "syscall" 18 | "time" 19 | 20 | "github.com/go-kit/log" 21 | "github.com/miekg/dns" 22 | "github.com/oklog/run" 23 | "github.com/quic-go/quic-go" 24 | ) 25 | 26 | func main() { 27 | l := log.NewLogfmtLogger(log.NewSyncWriter(os.Stdout)) 28 | l = log.WithPrefix(l, "ts", log.DefaultTimestampUTC) 29 | 30 | var g run.Group 31 | 32 | // proxy code loop 33 | { 34 | ctx, cancel := context.WithCancel(context.Background()) 35 | defer cancel() 36 | 37 | g.Add(func() error { 38 | return loop(l, ctx) 39 | }, func(error) { 40 | cancel() 41 | }) 42 | } 43 | 44 | // signal termination 45 | { 46 | sigterm := make(chan os.Signal, 1) 47 | g.Add(func() error { 48 | signal.Notify(sigterm, syscall.SIGINT, syscall.SIGTERM) 49 | if sig, ok := <-sigterm; ok { 50 | l.Log("msg", "stopping the proxy", "signal", sig.String()) 51 | } 52 | return nil 53 | }, func(error) { 54 | signal.Stop(sigterm) 55 | close(sigterm) 56 | }) 57 | } 58 | 59 | err := g.Run() 60 | if err != nil { 61 | l.Log("msg", "terminating after error", "err", err) 62 | os.Exit(1) 63 | } 64 | } 65 | 66 | func loop(l log.Logger, ctx context.Context) error { 67 | var ( 68 | addr string 69 | tlsCert string 70 | tlsKey string 71 | backend string 72 | mtlsCACerts string 73 | ) 74 | 75 | flag.StringVar(&addr, "listen", "127.0.0.1:853", "UDP address to listen on.") 76 | flag.StringVar(&tlsCert, "cert", "server.crt", "Path to server TLS certificate.") 77 | flag.StringVar(&tlsKey, "key", "server.key", "Path to server TLS key.") 78 | flag.StringVar(&backend, "backend", "8.8.4.4:53", "IP of backend server.") 79 | flag.StringVar(&mtlsCACerts, "mtls_ca_certs", "", "Path to CA bundle for mTLS.") 80 | 81 | flag.Parse() 82 | 83 | cert, err := tls.LoadX509KeyPair(tlsCert, tlsKey) 84 | if err != nil { 85 | return fmt.Errorf("load certificate: %w", err) 86 | } 87 | 88 | tlsConfig := tls.Config{ 89 | Certificates: []tls.Certificate{cert}, 90 | NextProtos: []string{"doq"}, 91 | } 92 | 93 | if mtlsCACerts != "" { 94 | pems, err := os.ReadFile(mtlsCACerts) 95 | if err != nil { 96 | return fmt.Errorf("load mTLS CA certificates: %w", err) 97 | } 98 | pool := x509.NewCertPool() 99 | if ok := pool.AppendCertsFromPEM(pems); !ok { 100 | return fmt.Errorf("load mTLS CA certificates: found no certificate") 101 | } 102 | 103 | tlsConfig.ClientCAs = pool 104 | tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert 105 | 106 | } 107 | 108 | listener, err := quic.ListenAddr(addr, &tlsConfig, nil) 109 | if err != nil { 110 | return fmt.Errorf("listen: %w", err) 111 | } 112 | defer listener.Close() 113 | 114 | l.Log("msg", "listening for clients", "addr", addr) 115 | 116 | wg := sync.WaitGroup{} 117 | 118 | for { 119 | session, err := listener.Accept(ctx) 120 | if err != nil { 121 | wg.Wait() 122 | return fmt.Errorf("accept connection: %w", err) 123 | } 124 | 125 | l := log.With(l, "client", session.RemoteAddr()) 126 | 127 | certs := session.ConnectionState().TLS.PeerCertificates 128 | if len(certs) > 0 { 129 | l = log.With(l, "client_cert_subject", certs[0].Subject) 130 | } 131 | 132 | wg.Add(1) 133 | go func() { 134 | handleClient(l, ctx, session, backend) 135 | wg.Done() 136 | }() 137 | } 138 | 139 | } 140 | 141 | func handleClient(l log.Logger, ctx context.Context, session quic.Connection, backend string) { 142 | l.Log("msg", "session accepted") 143 | 144 | var ( 145 | err error 146 | wg sync.WaitGroup = sync.WaitGroup{} 147 | ) 148 | 149 | defer func() { 150 | msg := "" 151 | if err != nil { 152 | msg = err.Error() 153 | } 154 | session.CloseWithError(0, msg) 155 | 156 | l.Log("msg", "session closed") 157 | }() 158 | 159 | for { 160 | stream, err := session.AcceptStream(ctx) 161 | if err != nil { 162 | break 163 | } 164 | 165 | l := log.With(l, "stream_id", stream.StreamID()) 166 | l.Log("msg", "stream accepted") 167 | 168 | wg.Add(1) 169 | go func() { 170 | defer func() { 171 | wg.Done() 172 | l.Log("msg", "stream closed") 173 | }() 174 | 175 | if err := handleStream(stream, backend); err != nil { 176 | l.Log("msg", "stream failure", "err", err) 177 | } 178 | }() 179 | } 180 | 181 | wg.Wait() 182 | } 183 | 184 | func handleStream(stream quic.Stream, backend string) error { 185 | defer stream.Close() 186 | 187 | wireLength := make([]byte, 2) 188 | _, err := io.ReadFull(stream, wireLength) 189 | if err != nil { 190 | return fmt.Errorf("read query length: %w", err) 191 | } 192 | 193 | length := binary.BigEndian.Uint16(wireLength) 194 | 195 | wireQuery := make([]byte, length) 196 | _, err = io.ReadFull(stream, wireQuery) 197 | if err != nil { 198 | return fmt.Errorf("read query payload: %w", err) 199 | } 200 | 201 | query := dns.Msg{} 202 | err = query.Unpack(wireQuery) 203 | if err != nil { 204 | return fmt.Errorf("could not decode query: %w", err) 205 | } 206 | 207 | var id uint16 208 | err = binary.Read(rand.Reader, binary.BigEndian, &id) 209 | if err != nil { 210 | return fmt.Errorf("generating random id failed: %w", err) 211 | } 212 | 213 | if len(query.Question) != 0 && (query.Question[0].Qtype == dns.TypeAXFR || query.Question[0].Qtype == dns.TypeIXFR) { 214 | timeout := 3 * time.Second 215 | conn, err := net.DialTimeout("tcp", backend, timeout) 216 | 217 | if err != nil { 218 | return fmt.Errorf("connect to TCP backend: %w", err) 219 | } 220 | defer conn.Close() 221 | 222 | bundle := make([]byte, 0) 223 | bundle = append(bundle, wireLength...) 224 | bundle = append(bundle, wireQuery...) 225 | 226 | binary.BigEndian.PutUint16(bundle[2:], uint16(id)) 227 | _, err = conn.Write(bundle) 228 | if err != nil { 229 | return fmt.Errorf("send query to TCP backend: %w", err) 230 | } 231 | 232 | conn.SetReadDeadline(time.Now().Add(timeout)) 233 | 234 | for { 235 | var length uint16 236 | if err := binary.Read(conn, binary.BigEndian, &length); err != nil { 237 | // Ignore timeout related errors as that is how we close the connection for now 238 | if errors.Is(err, os.ErrDeadlineExceeded) { 239 | return nil 240 | } 241 | return fmt.Errorf("read length from TCP backend: %w", err) 242 | } 243 | 244 | buf := make([]byte, length) 245 | _, err := io.ReadFull(conn, buf) 246 | if err != nil { 247 | return fmt.Errorf("read response from TCP backend: %w", err) 248 | } 249 | 250 | bundle := make([]byte, 2+length) 251 | binary.BigEndian.PutUint16(bundle, uint16(length)) 252 | copy(bundle[2:], buf) 253 | binary.BigEndian.PutUint16(bundle[2:], uint16(0)) 254 | _, err = stream.Write(bundle) 255 | if err != nil { 256 | return fmt.Errorf("send response: %w", err) 257 | } 258 | } 259 | 260 | } else { 261 | conn, err := net.Dial("udp", backend) 262 | if err != nil { 263 | return fmt.Errorf("connect to UDP backend: %w", err) 264 | } 265 | 266 | binary.BigEndian.PutUint16(wireQuery, uint16(id)) 267 | _, err = conn.Write(wireQuery) 268 | if err != nil { 269 | return fmt.Errorf("send query to UDP backend: %w", err) 270 | } 271 | 272 | buf := make([]byte, 4096) 273 | size, err := conn.Read(buf) 274 | if err != nil { 275 | return fmt.Errorf("read response from UDP backend: %w", err) 276 | } 277 | buf = buf[:size] 278 | 279 | bundle := make([]byte, 2+len(buf)) 280 | binary.BigEndian.PutUint16(bundle, uint16(len(buf))) 281 | copy(bundle[2:], buf) 282 | binary.BigEndian.PutUint16(bundle[2:], uint16(0)) 283 | 284 | _, err = stream.Write(bundle) 285 | if err != nil { 286 | return fmt.Errorf("send response: %w", err) 287 | } 288 | } 289 | return nil 290 | } 291 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------