-
Notifications
You must be signed in to change notification settings - Fork 62
/
dohlistener.go
298 lines (260 loc) · 7.34 KB
/
dohlistener.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
package rdns
import (
"context"
"crypto/tls"
"encoding/base64"
"expvar"
"fmt"
"io/ioutil"
"net"
"net/http"
"strings"
"time"
"github.com/miekg/dns"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3"
"github.com/sirupsen/logrus"
)
// Read/Write timeout in the DoH server
const dohServerTimeout = 10 * time.Second
// DoHListener is a DNS listener/server for DNS-over-HTTPS.
type DoHListener struct {
httpServer *http.Server
quicServer *http3.Server
id string
addr string
r Resolver
opt DoHListenerOptions
handler http.Handler
metrics *DoHListenerMetrics
}
var _ Listener = &DoHListener{}
// DoHListenerOptions contains options used by the DNS-over-HTTPS server.
type DoHListenerOptions struct {
ListenOptions
// Transport protocol to run HTTPS over. "quic" or "tcp", defaults to "tcp".
Transport string
TLSConfig *tls.Config
// IP(v4/v6) subnet of known reverse proxies in front of this server.
HTTPProxyNet *net.IPNet
// Disable TLS on the server (insecure, for testing purposes only).
NoTLS bool
}
type DoHListenerMetrics struct {
ListenerMetrics
// HTTP method used for query.
get *expvar.Int
post *expvar.Int
}
func NewDoHListenerMetrics(id string) *DoHListenerMetrics {
return &DoHListenerMetrics{
ListenerMetrics: ListenerMetrics{
query: getVarInt("listener", id, "query"),
response: getVarMap("listener", id, "response"),
err: getVarMap("listener", id, "error"),
drop: getVarInt("listener", id, "drop"),
},
get: getVarInt("listener", id, "get"),
post: getVarInt("listener", id, "post"),
}
}
// NewDoHListener returns an instance of a DNS-over-HTTPS listener.
func NewDoHListener(id, addr string, opt DoHListenerOptions, resolver Resolver) (*DoHListener, error) {
switch opt.Transport {
case "tcp", "":
opt.Transport = "tcp"
case "quic":
opt.Transport = "quic"
default:
return nil, fmt.Errorf("unknown protocol: '%s'", opt.Transport)
}
l := &DoHListener{
id: id,
addr: addr,
r: resolver,
opt: opt,
metrics: NewDoHListenerMetrics(id),
}
l.handler = http.HandlerFunc(l.dohHandler)
return l, nil
}
// Start the DoH server.
func (s *DoHListener) Start() error {
Log.WithFields(logrus.Fields{"id": s.id, "protocol": "doh", "addr": s.addr}).Info("starting listener")
if s.opt.Transport == "quic" {
return s.startQUIC()
}
return s.startTCP()
}
// Start the DoH server with TCP transport.
func (s *DoHListener) startTCP() error {
s.httpServer = &http.Server{
Addr: s.addr,
TLSConfig: s.opt.TLSConfig,
Handler: s.handler,
ReadTimeout: dohServerTimeout,
WriteTimeout: dohServerTimeout,
}
ln, err := net.Listen("tcp", s.addr)
if err != nil {
return err
}
defer ln.Close()
if s.opt.NoTLS {
return s.httpServer.Serve(ln)
}
return s.httpServer.ServeTLS(ln, "", "")
}
// Start the DoH server with QUIC transport.
func (s *DoHListener) startQUIC() error {
s.quicServer = &http3.Server{
Addr: s.addr,
TLSConfig: s.opt.TLSConfig,
Handler: s.handler,
QUICConfig: &quic.Config{},
}
return s.quicServer.ListenAndServe()
}
// Stop the server.
func (s *DoHListener) Stop() error {
Log.WithFields(logrus.Fields{"id": s.id, "protocol": "doh", "addr": s.addr}).Info("stopping listener")
if s.opt.Transport == "quic" {
return s.quicServer.Close()
}
return s.httpServer.Shutdown(context.Background())
}
func (s *DoHListener) String() string {
return s.id
}
func (s *DoHListener) dohHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
s.metrics.get.Add(1)
s.getHandler(w, r)
case "POST":
s.metrics.post.Add(1)
s.postHandler(w, r)
default:
s.metrics.err.Add("httpmethod", 1)
http.Error(w, "only GET and POST allowed", http.StatusMethodNotAllowed)
}
}
func (s *DoHListener) getHandler(w http.ResponseWriter, r *http.Request) {
b64, ok := r.URL.Query()["dns"]
if !ok {
http.Error(w, "no dns query parameter found", http.StatusBadRequest)
return
}
if len(b64) < 1 {
http.Error(w, "no dns query value found", http.StatusBadRequest)
return
}
b, err := base64.RawURLEncoding.DecodeString(b64[0])
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s.parseAndRespond(b, w, r)
}
func (s *DoHListener) postHandler(w http.ResponseWriter, r *http.Request) {
b, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
s.parseAndRespond(b, w, r)
}
// Extract the client address from the HTTP headers, accounting for known
// reverse proxies.
func (s *DoHListener) extractClientAddress(r *http.Request) net.IP {
client, _, _ := net.SplitHostPort(r.RemoteAddr)
clientIP := net.ParseIP(client)
// TODO: Prefer RFC 7239 Forwarded once https://github.com/golang/go/issues/30963
// is resolved and provides a safe parser.
xForwardedFor := r.Header.Get("X-Forwarded-For")
// Simple case: No proxy (or empty/long X-Forwarded-For).
if s.opt.HTTPProxyNet == nil || xForwardedFor == "" || len(xForwardedFor) >= 1024 {
return clientIP
}
// If our client is a reverse proxy then use the last entry in X-Forwarded-For.
chain := strings.Split(xForwardedFor, ", ")
if clientIP != nil && s.opt.HTTPProxyNet.Contains(clientIP) {
if ip := net.ParseIP(chain[len(chain)-1]); ip != nil {
// Ignore XFF whe the client is local to the proxy.
if !ip.IsLoopback() {
return ip
}
}
}
// TODO: Decide whether to go deeper into the XFF chain (eg. two reverse proxies).
// We have to be careful if we do, because then we're trusting an XFF that
// may have been provided externally.
return clientIP
}
func (s *DoHListener) parseAndRespond(b []byte, w http.ResponseWriter, r *http.Request) {
s.metrics.query.Add(1)
q := new(dns.Msg)
if err := q.Unpack(b); err != nil {
s.metrics.err.Add("unpack", 1)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Extract the remote host address from the HTTP headers.
clientIP := s.extractClientAddress(r)
if clientIP == nil {
s.metrics.err.Add("remoteaddr", 1)
http.Error(w, "Invalid RemoteAddr", http.StatusBadRequest)
return
}
var tlsServerName string
if r.TLS != nil {
tlsServerName = r.TLS.ServerName
}
ci := ClientInfo{
SourceIP: clientIP,
DoHPath: r.URL.Path,
TLSServerName: tlsServerName,
Listener: s.id,
}
log := Log.WithFields(logrus.Fields{
"id": s.id,
"client": ci.SourceIP,
"qtype": qType(q),
"qname": qName(q),
"protocol": "doh",
"addr": s.addr,
"path": r.URL.Path,
})
log.Debug("received query")
var err error
a := new(dns.Msg)
if isAllowed(s.opt.AllowedNet, ci.SourceIP) {
log.WithField("resolver", s.r.String()).Debug("forwarding query to resolver")
a, err = s.r.Resolve(q, ci)
if err != nil {
log.WithError(err).Error("failed to resolve")
a = new(dns.Msg)
a.SetRcode(q, dns.RcodeServerFailure)
}
} else {
log.Debug("refusing client ip")
a.SetRcode(q, dns.RcodeRefused)
}
// A nil response from the resolvers means "drop", return blank response
if a == nil {
s.metrics.drop.Add(1)
w.WriteHeader(http.StatusForbidden)
return
}
// Pad the packet according to rfc8467 and rfc7830
padAnswer(q, a)
s.metrics.response.Add(rCode(a), 1)
out, err := a.Pack()
if err != nil {
s.metrics.err.Add("pack", 1)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("content-type", "application/dns-message")
_, _ = w.Write(out)
}