@ -0,0 +1,36 @@ | |||
package main | |||
import ( | |||
"flag" | |||
"strings" | |||
"maze.io/doh" | |||
) | |||
const ( | |||
// Default using dns.maze.network DoT (DNS over TLS) resolver. | |||
defaultUpstream = "tcp-tls://dns.maze.network:853,udp://178.33.2.152:53" | |||
) | |||
func main() { | |||
var ( | |||
listen = flag.String("listen", ":8053", "listen address") | |||
upstream = flag.String("upstream", defaultUpstream, "upstream DNS servers") | |||
insecure = flag.Bool("insecure", false, "skip TLS verification from upstream") | |||
certFile = flag.String("cert", "", "X.509 certificate file") | |||
keyFile = flag.String("key", "", "X.509 key file") | |||
) | |||
flag.Parse() | |||
server := doh.NewServer() | |||
server.Insecure = *insecure | |||
server.Upstream = strings.Split(*upstream, ",") | |||
if *certFile != "" { | |||
if *keyFile == "" { | |||
*keyFile = *certFile | |||
} | |||
server.StartTLS(*listen, *certFile, *keyFile) | |||
} else { | |||
server.Start(*listen) | |||
} | |||
} |
@ -0,0 +1,104 @@ | |||
package doh | |||
import "net" | |||
// RFC6890 | |||
var localIPv4Nets = []net.IPNet{ | |||
// This host on this network | |||
net.IPNet{ | |||
net.IP{0, 0, 0, 0}, | |||
net.IPMask{255, 0, 0, 0}, | |||
}, | |||
// Private-Use Networks | |||
net.IPNet{ | |||
net.IP{10, 0, 0, 0}, | |||
net.IPMask{255, 0, 0, 0}, | |||
}, | |||
// Shared Address Space | |||
net.IPNet{ | |||
net.IP{100, 64, 0, 0}, | |||
net.IPMask{255, 192, 0, 0}, | |||
}, | |||
// Loopback | |||
net.IPNet{ | |||
net.IP{127, 0, 0, 0}, | |||
net.IPMask{255, 0, 0, 0}, | |||
}, | |||
// Link Local | |||
net.IPNet{ | |||
net.IP{169, 254, 0, 0}, | |||
net.IPMask{255, 255, 0, 0}, | |||
}, | |||
// Private-Use Networks | |||
net.IPNet{ | |||
net.IP{172, 16, 0, 0}, | |||
net.IPMask{255, 240, 0, 0}, | |||
}, | |||
// DS-Lite | |||
net.IPNet{ | |||
net.IP{192, 0, 0, 0}, | |||
net.IPMask{255, 255, 255, 248}, | |||
}, | |||
// 6to4 Relay Anycast | |||
net.IPNet{ | |||
net.IP{192, 88, 99, 0}, | |||
net.IPMask{255, 255, 255, 0}, | |||
}, | |||
// Private-Use Networks | |||
net.IPNet{ | |||
net.IP{192, 168, 0, 0}, | |||
net.IPMask{255, 255, 0, 0}, | |||
}, | |||
// Reserved for Future Use & Limited Broadcast | |||
net.IPNet{ | |||
net.IP{240, 0, 0, 0}, | |||
net.IPMask{240, 0, 0, 0}, | |||
}, | |||
} | |||
// RFC6890 | |||
var localIPv6Nets = []net.IPNet{ | |||
// Unspecified & Loopback Address | |||
net.IPNet{ | |||
net.IP{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, | |||
net.IPMask{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe}, | |||
}, | |||
// Discard-Only Prefix | |||
net.IPNet{ | |||
net.IP{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, | |||
net.IPMask{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, | |||
}, | |||
// Unique-Local | |||
net.IPNet{ | |||
net.IP{0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, | |||
net.IPMask{0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, | |||
}, | |||
// Linked-Scoped Unicast | |||
net.IPNet{ | |||
net.IP{0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, | |||
net.IPMask{0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, | |||
}, | |||
} | |||
func isGlobalIP(ip net.IP) bool { | |||
if ip == nil { | |||
return false | |||
} | |||
if ipv4 := ip.To4(); len(ipv4) == net.IPv4len { | |||
for _, cidr := range localIPv4Nets { | |||
if cidr.Contains(ip) { | |||
return false | |||
} | |||
} | |||
return true | |||
} | |||
if len(ip) == net.IPv6len { | |||
for _, cidr := range localIPv6Nets { | |||
if cidr.Contains(ip) { | |||
return false | |||
} | |||
} | |||
return true | |||
} | |||
return true | |||
} |
@ -0,0 +1,11 @@ | |||
module maze.io/doh | |||
go 1.13 | |||
require ( | |||
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect | |||
github.com/labstack/echo/v4 v4.1.15 | |||
github.com/miekg/dns v1.1.27 | |||
github.com/sirupsen/logrus v1.4.2 | |||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b | |||
) |
@ -0,0 +1,63 @@ | |||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | |||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | |||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= | |||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= | |||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= | |||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= | |||
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= | |||
github.com/labstack/echo/v4 v4.1.15 h1:4aE6KfJC+wCnMjODwcpeEGWGsRfszxZMwB3QVTECj2I= | |||
github.com/labstack/echo/v4 v4.1.15/go.mod h1:GWO5IBVzI371K8XJe50CSvHjQCafK6cw8R/moLhEU6o= | |||
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= | |||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= | |||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= | |||
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= | |||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= | |||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= | |||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= | |||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= | |||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= | |||
github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM= | |||
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= | |||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | |||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= | |||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= | |||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | |||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | |||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= | |||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | |||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= | |||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= | |||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= | |||
github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4= | |||
github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= | |||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | |||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= | |||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | |||
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw= | |||
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | |||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= | |||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | |||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | |||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g= | |||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | |||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= | |||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | |||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | |||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | |||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M= | |||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= | |||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | |||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= | |||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= | |||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | |||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= | |||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | |||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
@ -0,0 +1,277 @@ | |||
package doh | |||
import ( | |||
"fmt" | |||
"log" | |||
"net" | |||
"strconv" | |||
"strings" | |||
"time" | |||
"github.com/miekg/dns" | |||
) | |||
type Response struct { | |||
Status uint32 `json:"Status"` // Standard DNS response code (32 bit integer) | |||
TC bool `json:"TC"` // Whether the response is truncated | |||
RD bool `json:"RD"` // Recursion desired | |||
RA bool `json:"RA"` // Recursion available | |||
AD bool `json:"AD"` // Whether all response data was validated with DNSSEC | |||
CD bool `json:"CD"` // Whether the client asked to disable DNSSEC | |||
Question []Question `json:"Question"` | |||
Answer []RR `json:"Answer,omitempty"` | |||
Authority []RR `json:"Authority,omitempty"` | |||
Additional []RR `json:"Additional,omitempty"` | |||
Comment string `json:"Comment,omitempty"` | |||
EdnsClientSubnet string `json:"edns_client_subnet,omitempty"` | |||
HaveTTL bool `json:"-"` // Least time-to-live | |||
LeastTTL uint32 `json:"-"` | |||
EarliestExpires time.Time `json:"-"` | |||
} | |||
type Question struct { | |||
Name string `json:"name"` // FQDN with trailing dot | |||
Type uint16 `json:"type"` // Standard DNS RR type | |||
} | |||
type RR struct { | |||
Question | |||
TTL uint32 `json:"TTL"` // Record's time-to-live in seconds | |||
Expires time.Time `json:"-"` // TTL in absolute time | |||
ExpiresStr string `json:"Expires"` | |||
Data string `json:"data"` // Data | |||
} | |||
func Marshal(msg *dns.Msg) *Response { | |||
now := time.Now().UTC() | |||
resp := new(Response) | |||
resp.Status = uint32(msg.Rcode) | |||
resp.TC = msg.Truncated | |||
resp.RD = msg.RecursionDesired | |||
resp.RA = msg.RecursionAvailable | |||
resp.AD = msg.AuthenticatedData | |||
resp.CD = msg.CheckingDisabled | |||
resp.Question = make([]Question, 0, len(msg.Question)) | |||
for _, question := range msg.Question { | |||
jsonQuestion := Question{ | |||
Name: question.Name, | |||
Type: question.Qtype, | |||
} | |||
resp.Question = append(resp.Question, jsonQuestion) | |||
} | |||
resp.Answer = make([]RR, 0, len(msg.Answer)) | |||
for _, rr := range msg.Answer { | |||
jsonAnswer := marshalRR(rr, now) | |||
if !resp.HaveTTL || jsonAnswer.TTL < resp.LeastTTL { | |||
resp.HaveTTL = true | |||
resp.LeastTTL = jsonAnswer.TTL | |||
resp.EarliestExpires = jsonAnswer.Expires | |||
} | |||
resp.Answer = append(resp.Answer, jsonAnswer) | |||
} | |||
resp.Authority = make([]RR, 0, len(msg.Ns)) | |||
for _, rr := range msg.Ns { | |||
jsonAuthority := marshalRR(rr, now) | |||
if !resp.HaveTTL || jsonAuthority.TTL < resp.LeastTTL { | |||
resp.HaveTTL = true | |||
resp.LeastTTL = jsonAuthority.TTL | |||
resp.EarliestExpires = jsonAuthority.Expires | |||
} | |||
resp.Authority = append(resp.Authority, jsonAuthority) | |||
} | |||
resp.Additional = make([]RR, 0, len(msg.Extra)) | |||
for _, rr := range msg.Extra { | |||
jsonAdditional := marshalRR(rr, now) | |||
header := rr.Header() | |||
if header.Rrtype == dns.TypeOPT { | |||
opt := rr.(*dns.OPT) | |||
resp.Status = ((opt.Hdr.Ttl & 0xff000000) >> 20) | (resp.Status & 0xff) | |||
for _, option := range opt.Option { | |||
if option.Option() == dns.EDNS0SUBNET { | |||
edns0 := option.(*dns.EDNS0_SUBNET) | |||
clientAddress := edns0.Address | |||
if clientAddress == nil { | |||
clientAddress = net.IP{0, 0, 0, 0} | |||
} else if ipv4 := clientAddress.To4(); ipv4 != nil { | |||
clientAddress = ipv4 | |||
} | |||
resp.EdnsClientSubnet = clientAddress.String() + "/" + strconv.FormatUint(uint64(edns0.SourceScope), 10) | |||
} | |||
} | |||
continue | |||
} | |||
if !resp.HaveTTL || jsonAdditional.TTL < resp.LeastTTL { | |||
resp.HaveTTL = true | |||
resp.LeastTTL = jsonAdditional.TTL | |||
resp.EarliestExpires = jsonAdditional.Expires | |||
} | |||
resp.Additional = append(resp.Additional, jsonAdditional) | |||
} | |||
return resp | |||
} | |||
func marshalRR(rr dns.RR, now time.Time) (out RR) { | |||
rrHeader := rr.Header() | |||
out.Name = rrHeader.Name | |||
out.Type = rrHeader.Rrtype | |||
out.TTL = rrHeader.Ttl | |||
out.Expires = now.Add(time.Duration(out.TTL) * time.Second) | |||
out.ExpiresStr = out.Expires.Format(time.RFC1123) | |||
data := strings.SplitN(rr.String(), "\t", 5) | |||
if len(data) >= 5 { | |||
out.Data = data[4] | |||
} | |||
return out | |||
} | |||
func Unmarshal(msg *dns.Msg, resp *Response, udpSize uint16, ednsClientNetmask uint8) *dns.Msg { | |||
now := time.Now().UTC() | |||
reply := msg.Copy() | |||
reply.Truncated = resp.TC | |||
reply.AuthenticatedData = resp.AD | |||
reply.CheckingDisabled = resp.CD | |||
reply.Rcode = dns.RcodeServerFailure | |||
reply.Answer = make([]dns.RR, 0, len(resp.Answer)) | |||
for _, rr := range resp.Answer { | |||
dnsRR, err := unmarshalRR(rr, now) | |||
if err != nil { | |||
log.Println(err) | |||
} else { | |||
reply.Answer = append(reply.Answer, dnsRR) | |||
} | |||
} | |||
reply.Ns = make([]dns.RR, 0, len(resp.Authority)) | |||
for _, rr := range resp.Authority { | |||
dnsRR, err := unmarshalRR(rr, now) | |||
if err != nil { | |||
log.Println(err) | |||
} else { | |||
reply.Ns = append(reply.Ns, dnsRR) | |||
} | |||
} | |||
reply.Extra = make([]dns.RR, 0, len(resp.Additional)+1) | |||
opt := new(dns.OPT) | |||
opt.Hdr.Name = "." | |||
opt.Hdr.Rrtype = dns.TypeOPT | |||
if udpSize >= 512 { | |||
opt.SetUDPSize(udpSize) | |||
} else { | |||
opt.SetUDPSize(512) | |||
} | |||
opt.SetDo(false) | |||
ednsClientSubnet := resp.EdnsClientSubnet | |||
ednsClientFamily := uint16(0) | |||
ednsClientAddress := net.IP(nil) | |||
ednsClientScope := uint8(255) | |||
if ednsClientSubnet != "" { | |||
slash := strings.IndexByte(ednsClientSubnet, '/') | |||
if slash < 0 { | |||
log.Println(UnmarshalError{"Invalid client subnet"}) | |||
} else { | |||
ednsClientAddress = net.ParseIP(ednsClientSubnet[:slash]) | |||
if ednsClientAddress == nil { | |||
log.Println(UnmarshalError{"Invalid client subnet address"}) | |||
} else if ipv4 := ednsClientAddress.To4(); ipv4 != nil { | |||
ednsClientFamily = 1 | |||
ednsClientAddress = ipv4 | |||
} else { | |||
ednsClientFamily = 2 | |||
} | |||
scope, err := strconv.ParseUint(ednsClientSubnet[slash+1:], 10, 8) | |||
if err != nil { | |||
log.Println(UnmarshalError{"Invalid client subnet address"}) | |||
} else { | |||
ednsClientScope = uint8(scope) | |||
} | |||
} | |||
} | |||
if ednsClientAddress != nil { | |||
if ednsClientNetmask == 255 { | |||
if ednsClientFamily == 1 { | |||
ednsClientNetmask = 24 | |||
} else { | |||
ednsClientNetmask = 56 | |||
} | |||
} | |||
edns0Subnet := new(dns.EDNS0_SUBNET) | |||
edns0Subnet.Code = dns.EDNS0SUBNET | |||
edns0Subnet.Family = ednsClientFamily | |||
edns0Subnet.SourceNetmask = ednsClientNetmask | |||
edns0Subnet.SourceScope = ednsClientScope | |||
edns0Subnet.Address = ednsClientAddress | |||
opt.Option = append(opt.Option, edns0Subnet) | |||
} | |||
reply.Extra = append(reply.Extra, opt) | |||
for _, rr := range resp.Additional { | |||
dnsRR, err := unmarshalRR(rr, now) | |||
if err != nil { | |||
log.Println(err) | |||
} else { | |||
reply.Extra = append(reply.Extra, dnsRR) | |||
} | |||
} | |||
reply.Rcode = int(resp.Status & 0xf) | |||
opt.Hdr.Ttl = (opt.Hdr.Ttl & 0x00ffffff) | ((resp.Status & 0xff0) << 20) | |||
reply.Extra[0] = opt | |||
return reply | |||
} | |||
func unmarshalRR(rr RR, now time.Time) (dnsRR dns.RR, err error) { | |||
if strings.ContainsAny(rr.Name, "\t\r\n \"();\\") { | |||
return nil, UnmarshalError{fmt.Sprintf("Record name contains space: %q", rr.Name)} | |||
} | |||
if rr.ExpiresStr != "" { | |||
rr.Expires, err = time.Parse(time.RFC1123, rr.ExpiresStr) | |||
if err != nil { | |||
return nil, UnmarshalError{fmt.Sprintf("Invalid expire time: %q", rr.ExpiresStr)} | |||
} | |||
ttl := rr.Expires.Sub(now) / time.Second | |||
if ttl >= 0 && ttl <= 0xffffffff { | |||
rr.TTL = uint32(ttl) | |||
} | |||
} | |||
rrType, ok := dns.TypeToString[rr.Type] | |||
if !ok { | |||
return nil, UnmarshalError{fmt.Sprintf("Unknown record type: %d", rr.Type)} | |||
} | |||
if strings.ContainsAny(rr.Data, "\r\n") { | |||
return nil, UnmarshalError{fmt.Sprintf("Record data contains newline: %q", rr.Data)} | |||
} | |||
zone := fmt.Sprintf("%s %d IN %s %s", rr.Name, rr.TTL, rrType, rr.Data) | |||
dnsRR, err = dns.NewRR(zone) | |||
return | |||
} | |||
type UnmarshalError struct { | |||
err string | |||
} | |||
func (e UnmarshalError) Error() string { | |||
return "json-dns: " + e.err | |||
} | |||
func PrepareReply(req *dns.Msg) *dns.Msg { | |||
reply := new(dns.Msg) | |||
reply.Id = req.Id | |||
reply.Response = true | |||
reply.Opcode = req.Opcode | |||
reply.RecursionDesired = req.RecursionDesired | |||
reply.RecursionAvailable = req.RecursionDesired | |||
reply.CheckingDisabled = req.CheckingDisabled | |||
reply.Rcode = dns.RcodeServerFailure | |||
reply.Compress = true | |||
reply.Question = make([]dns.Question, len(req.Question)) | |||
copy(reply.Question, req.Question) | |||
return reply | |||
} |
@ -0,0 +1,313 @@ | |||
package doh | |||
import ( | |||
"context" | |||
"crypto/tls" | |||
"encoding/json" | |||
"fmt" | |||
"log" | |||
"net" | |||
"net/http" | |||
"net/url" | |||
"strings" | |||
"time" | |||
"github.com/sirupsen/logrus" | |||
"github.com/miekg/dns" | |||
"github.com/labstack/echo/v4" | |||
"github.com/labstack/echo/v4/middleware" | |||
) | |||
const queryTimeout = 5 * time.Second | |||
type Server struct { | |||
*echo.Echo | |||
// Upstream DNS recursors | |||
Upstream []string | |||
// Insecure disables TLS verification | |||
Insecure bool | |||
// Verbose logging | |||
Verbose bool | |||
LogGuessedIP bool | |||
udpClient *dns.Client | |||
tcpClient *dns.Client | |||
tcpClientTLS *dns.Client | |||
} | |||
func NewServer() *Server { | |||
server := &Server{ | |||
Echo: echo.New(), | |||
udpClient: &dns.Client{ | |||
Net: "udp", | |||
UDPSize: dns.DefaultMsgSize, | |||
Timeout: queryTimeout, | |||
}, | |||
tcpClient: &dns.Client{ | |||
Net: "tcp", | |||
Timeout: queryTimeout, | |||
}, | |||
tcpClientTLS: &dns.Client{ | |||
Net: "tcp-tls", | |||
Timeout: queryTimeout, | |||
TLSConfig: &tls.Config{}, | |||
}, | |||
} | |||
// Middleware | |||
//server.Use(middleware.Recover()) | |||
server.Use(middleware.Logger()) | |||
// Routes | |||
server.GET("/dns-query", server.handleDNSQuery) | |||
server.POST("/dns-query", server.handleDNSQuery) | |||
return server | |||
} | |||
func (server *Server) handleDNSQuery(c echo.Context) error { | |||
var ( | |||
r = c.Request() | |||
ctx = r.Context() | |||
response = c.Response() | |||
w = response.Writer | |||
header = response.Header() | |||
) | |||
header.Set("Access-Control-Allow-Headers", "Content-Type") | |||
header.Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS, POST") | |||
header.Set("Access-Control-Allow-Origin", "*") | |||
header.Set("Access-Control-Max-Age", "3600") | |||
//header.Set("Server", USER_AGENT) | |||
//header.Set("X-Powered-By", USER_AGENT) | |||
if r.Method == http.MethodOptions { | |||
header.Set("Content-Length", "0") | |||
return nil | |||
} | |||
if r.Form == nil { | |||
const maxMemory = 32 << 20 // 32 MB | |||
r.ParseMultipartForm(maxMemory) | |||
} | |||
contentType := r.Header.Get("Content-Type") | |||
if ct := r.FormValue("ct"); ct != "" { | |||
contentType = ct | |||
} | |||
if contentType == "" { | |||
// Guess request Content-Type based on other parameters | |||
if r.FormValue("name") != "" { | |||
contentType = "application/dns-json" | |||
} else if r.FormValue("dns") != "" { | |||
contentType = "application/dns-message" | |||
} | |||
} | |||
var responseType string | |||
for _, responseCandidate := range strings.Split(r.Header.Get("Accept"), ",") { | |||
responseCandidate = strings.SplitN(responseCandidate, ";", 2)[0] | |||
if responseCandidate == "application/json" { | |||
responseType = "application/json" | |||
break | |||
} else if responseCandidate == "application/dns-udpwireformat" { | |||
responseType = "application/dns-message" | |||
break | |||
} else if responseCandidate == "application/dns-message" { | |||
responseType = "application/dns-message" | |||
break | |||
} | |||
} | |||
if responseType == "" { | |||
// Guess response Content-Type based on request Content-Type | |||
if contentType == "application/dns-json" { | |||
responseType = "application/json" | |||
} else if contentType == "application/dns-message" { | |||
responseType = "application/dns-message" | |||
} else if contentType == "application/dns-udpwireformat" { | |||
responseType = "application/dns-message" | |||
} | |||
} | |||
var req *DNSRequest | |||
if contentType == "application/dns-json" { | |||
req = server.parseRequestGoogle(ctx, w, r) | |||
} else if contentType == "application/dns-message" { | |||
req = server.parseRequestIETF(ctx, w, r) | |||
} else if contentType == "application/dns-udpwireformat" { | |||
req = server.parseRequestIETF(ctx, w, r) | |||
} else { | |||
formatError(w, fmt.Sprintf("Invalid argument value: \"ct\" = %q", contentType), 415) | |||
return nil | |||
} | |||
if req.errcode == 444 { | |||
return nil | |||
} | |||
req = server.patchRootRD(req) | |||
var err error | |||
req, err = server.query(ctx, req) | |||
logger := logrus.WithFields(logrus.Fields{ | |||
"content_type": contentType, | |||
"request": formatQuestions(req.request.Question), | |||
"response": formatRRs(req.response.Answer), | |||
"response_type": responseType, | |||
}) | |||
if err != nil { | |||
logger.WithError(err).Warn("query failure") | |||
formatError(response, fmt.Sprintf("DNS query failure (%s)", err.Error()), 503) | |||
return nil | |||
} | |||
logger.Info("query success") | |||
if responseType == "application/json" { | |||
server.generateResponseGoogle(ctx, w, r, req) | |||
} else if responseType == "application/dns-message" { | |||
server.generateResponseIETF(ctx, w, r, req) | |||
} | |||
return nil | |||
} | |||
func (server *Server) query(ctx context.Context, req *DNSRequest) (res *DNSRequest, err error) { | |||
for _, upstream := range server.Upstream { | |||
var u *url.URL | |||
if u, err = url.Parse(upstream); err != nil { | |||
continue | |||
} | |||
switch u.Scheme { | |||
case "tcp-tls": | |||
server.tcpClientTLS.TLSConfig.InsecureSkipVerify = server.Insecure | |||
req.response, _, err = server.tcpClientTLS.Exchange(req.request, u.Host) | |||
case "tcp": | |||
req.response, _, err = server.tcpClient.Exchange(req.request, u.Host) | |||
case "udp": | |||
if server.indexQuestionType(req.request, dns.TypeAXFR) > -1 { | |||
req.response, _, err = server.tcpClient.Exchange(req.request, u.Host) | |||
} else { | |||
req.response, _, err = server.udpClient.Exchange(req.request, u.Host) | |||
if err == nil && req.response != nil && req.response.Truncated { | |||
log.Println(err) | |||
req.response, _, err = server.tcpClient.Exchange(req.request, u.Host) | |||
} | |||
// Retry with TCP if this was an IXFR request and we only received an SOA | |||
if (server.indexQuestionType(req.request, dns.TypeIXFR) > -1) && | |||
(len(req.response.Answer) == 1) && | |||
(req.response.Answer[0].Header().Rrtype == dns.TypeSOA) { | |||
req.response, _, err = server.tcpClient.Exchange(req.request, u.Host) | |||
} | |||
} | |||
} | |||
if err == nil { | |||
return req, nil | |||
} | |||
log.Printf("DNS error from upstream %s: %s\n", req.currentUpstream, err.Error()) | |||
} | |||
return | |||
} | |||
func (server *Server) indexQuestionType(msg *dns.Msg, qtype uint16) int { | |||
for i, question := range msg.Question { | |||
if question.Qtype == qtype { | |||
return i | |||
} | |||
} | |||
return -1 | |||
} | |||
func (server *Server) findClientIP(r *http.Request) net.IP { | |||
noEcs := r.URL.Query().Get("no_ecs") | |||
if strings.ToLower(noEcs) == "true" { | |||
return nil | |||
} | |||
XForwardedFor := r.Header.Get("X-Forwarded-For") | |||
if XForwardedFor != "" { | |||
for _, addr := range strings.Split(XForwardedFor, ",") { | |||
addr = strings.TrimSpace(addr) | |||
ip := net.ParseIP(addr) | |||
if isGlobalIP(ip) { | |||
return ip | |||
} | |||
} | |||
} | |||
XRealIP := r.Header.Get("X-Real-IP") | |||
if XRealIP != "" { | |||
addr := strings.TrimSpace(XRealIP) | |||
ip := net.ParseIP(addr) | |||
if isGlobalIP(ip) { | |||
return ip | |||
} | |||
} | |||
remoteAddr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr) | |||
if err != nil { | |||
return nil | |||
} | |||
if ip := remoteAddr.IP; isGlobalIP(ip) { | |||
return ip | |||
} | |||
return nil | |||
} | |||
func (server *Server) patchRootRD(req *DNSRequest) *DNSRequest { | |||
for _, question := range req.request.Question { | |||
if question.Name == "." { | |||
req.request.RecursionDesired = true | |||
} | |||
} | |||
return req | |||
} | |||
type DNSRequest struct { | |||
request *dns.Msg | |||
response *dns.Msg | |||
transactionID uint16 | |||
currentUpstream string | |||
isTailored bool | |||
errcode int | |||
errtext string | |||
} | |||
type dnsError struct { | |||
Status uint32 `json:"Status"` | |||
Comment string `json:"Comment,omitempty"` | |||
} | |||
func formatError(w http.ResponseWriter, comment string, errcode int) { | |||
w.Header().Set("Content-Type", "application/json; charset=UTF-8") | |||
errJson := dnsError{ | |||
Status: dns.RcodeServerFailure, | |||
Comment: comment, | |||
} | |||
errStr, err := json.Marshal(errJson) | |||
if err != nil { | |||
log.Fatalln(err) | |||
} | |||
w.WriteHeader(errcode) | |||
w.Write(errStr) | |||
} | |||
func formatQuestions(questions []dns.Question) []string { | |||
s := make([]string, len(questions)) | |||
for i, q := range questions { | |||
s[i] = q.String() | |||
} | |||
return s | |||
} | |||
func formatRRs(rrs []dns.RR) []string { | |||
s := make([]string, len(rrs)) | |||
for i, rr := range rrs { | |||
s[i] = rr.String() | |||
} | |||
return s | |||
} |
@ -0,0 +1,174 @@ | |||
package doh | |||
import ( | |||
"context" | |||
"encoding/json" | |||
"fmt" | |||
"log" | |||
"net" | |||
"net/http" | |||
"strconv" | |||
"strings" | |||
"time" | |||
"github.com/miekg/dns" | |||
"golang.org/x/net/idna" | |||
) | |||
func (server *Server) parseRequestGoogle(ctx context.Context, w http.ResponseWriter, r *http.Request) *DNSRequest { | |||
name := r.FormValue("name") | |||
if name == "" { | |||
return &DNSRequest{ | |||
errcode: 400, | |||
errtext: "Invalid argument value: \"name\"", | |||
} | |||
} | |||
if punycode, err := idna.ToASCII(name); err == nil { | |||
name = punycode | |||
} else { | |||
return &DNSRequest{ | |||
errcode: 400, | |||
errtext: fmt.Sprintf("Invalid argument value: \"name\" = %q (%s)", name, err.Error()), | |||
} | |||
} | |||
rrTypeStr := r.FormValue("type") | |||
rrType := uint16(1) | |||
if rrTypeStr == "" { | |||
} else if v, err := strconv.ParseUint(rrTypeStr, 10, 16); err == nil { | |||
rrType = uint16(v) | |||
} else if v, ok := dns.StringToType[strings.ToUpper(rrTypeStr)]; ok { | |||
rrType = v | |||
} else { | |||
return &DNSRequest{ | |||
errcode: 400, | |||
errtext: fmt.Sprintf("Invalid argument value: \"type\" = %q", rrTypeStr), | |||
} | |||
} | |||
cdStr := r.FormValue("cd") | |||
cd := false | |||
if cdStr == "1" || strings.EqualFold(cdStr, "true") { | |||
cd = true | |||
} else if cdStr == "0" || strings.EqualFold(cdStr, "false") || cdStr == "" { | |||
} else { | |||
return &DNSRequest{ | |||
errcode: 400, | |||
errtext: fmt.Sprintf("Invalid argument value: \"cd\" = %q", cdStr), | |||
} | |||
} | |||
ednsClientSubnet := r.FormValue("edns_client_subnet") | |||
ednsClientFamily := uint16(0) | |||
ednsClientAddress := net.IP(nil) | |||
ednsClientNetmask := uint8(255) | |||
if ednsClientSubnet != "" { | |||
if ednsClientSubnet == "0/0" { | |||
ednsClientSubnet = "0.0.0.0/0" | |||
} | |||
slash := strings.IndexByte(ednsClientSubnet, '/') | |||
if slash < 0 { | |||
ednsClientAddress = net.ParseIP(ednsClientSubnet) | |||
if ednsClientAddress == nil { | |||
return &DNSRequest{ | |||
errcode: 400, | |||
errtext: fmt.Sprintf("Invalid argument value: \"edns_client_subnet\" = %q", ednsClientSubnet), | |||
} | |||
} | |||
if ipv4 := ednsClientAddress.To4(); ipv4 != nil { | |||
ednsClientFamily = 1 | |||
ednsClientAddress = ipv4 | |||
ednsClientNetmask = 24 | |||
} else { | |||
ednsClientFamily = 2 | |||
ednsClientNetmask = 56 | |||
} | |||
} else { | |||
ednsClientAddress = net.ParseIP(ednsClientSubnet[:slash]) | |||
if ednsClientAddress == nil { | |||
return &DNSRequest{ | |||
errcode: 400, | |||
errtext: fmt.Sprintf("Invalid argument value: \"edns_client_subnet\" = %q", ednsClientSubnet), | |||
} | |||
} | |||
if ipv4 := ednsClientAddress.To4(); ipv4 != nil { | |||
ednsClientFamily = 1 | |||
ednsClientAddress = ipv4 | |||
} else { | |||
ednsClientFamily = 2 | |||
} | |||
netmask, err := strconv.ParseUint(ednsClientSubnet[slash+1:], 10, 8) | |||
if err != nil { | |||
return &DNSRequest{ | |||
errcode: 400, | |||
errtext: fmt.Sprintf("Invalid argument value: \"edns_client_subnet\" = %q", ednsClientSubnet), | |||
} | |||
} | |||
ednsClientNetmask = uint8(netmask) | |||
} | |||
} else { | |||
ednsClientAddress = server.findClientIP(r) | |||
if ednsClientAddress == nil { | |||
ednsClientNetmask = 0 | |||
} else if ipv4 := ednsClientAddress.To4(); ipv4 != nil { | |||
ednsClientFamily = 1 | |||
ednsClientAddress = ipv4 | |||
ednsClientNetmask = 24 | |||
} else { | |||
ednsClientFamily = 2 | |||
ednsClientNetmask = 56 | |||
} | |||
} | |||
msg := new(dns.Msg) | |||
msg.SetQuestion(dns.Fqdn(name), rrType) | |||
msg.CheckingDisabled = cd | |||
opt := new(dns.OPT) | |||
opt.Hdr.Name = "." | |||
opt.Hdr.Rrtype = dns.TypeOPT | |||
opt.SetUDPSize(dns.DefaultMsgSize) | |||
opt.SetDo(true) | |||
if ednsClientAddress != nil { | |||
edns0Subnet := new(dns.EDNS0_SUBNET) | |||
edns0Subnet.Code = dns.EDNS0SUBNET | |||
edns0Subnet.Family = ednsClientFamily | |||
edns0Subnet.SourceNetmask = ednsClientNetmask | |||
edns0Subnet.SourceScope = 0 | |||
edns0Subnet.Address = ednsClientAddress | |||
opt.Option = append(opt.Option, edns0Subnet) | |||
} | |||
msg.Extra = append(msg.Extra, opt) | |||
return &DNSRequest{ | |||
request: msg, | |||
isTailored: ednsClientSubnet == "", | |||
} | |||
} | |||
func (server *Server) generateResponseGoogle(ctx context.Context, w http.ResponseWriter, r *http.Request, req *DNSRequest) { | |||
respJSON := Marshal(req.response) | |||
respStr, err := json.Marshal(respJSON) | |||
if err != nil { | |||
log.Println(err) | |||
formatError(w, fmt.Sprintf("DNS packet parse failure (%s)", err.Error()), 500) | |||
return | |||
} | |||
w.Header().Set("Content-Type", "application/json; charset=UTF-8") | |||
now := time.Now().UTC().Format(http.TimeFormat) | |||
w.Header().Set("Date", now) | |||
w.Header().Set("Last-Modified", now) | |||
w.Header().Set("Vary", "Accept") | |||
if respJSON.HaveTTL { | |||
if req.isTailored { | |||
w.Header().Set("Cache-Control", "private, max-age="+strconv.FormatUint(uint64(respJSON.LeastTTL), 10)) | |||
} else { | |||
w.Header().Set("Cache-Control", "public, max-age="+strconv.FormatUint(uint64(respJSON.LeastTTL), 10)) | |||
} | |||
w.Header().Set("Expires", respJSON.EarliestExpires.Format(http.TimeFormat)) | |||
} | |||
if respJSON.Status == dns.RcodeServerFailure { | |||
w.WriteHeader(503) | |||
} | |||
w.Write(respStr) | |||
} |
@ -0,0 +1,195 @@ | |||
package doh | |||
import ( | |||
"bytes" | |||
"context" | |||
"encoding/base64" | |||
"fmt" | |||
"io/ioutil" | |||
"log" | |||
"net" | |||
"net/http" | |||
"strconv" | |||
"strings" | |||
"time" | |||
"github.com/miekg/dns" | |||
) | |||
func (server *Server) parseRequestIETF(ctx context.Context, w http.ResponseWriter, r *http.Request) *DNSRequest { | |||
requestBase64 := r.FormValue("dns") | |||
requestBinary, err := base64.RawURLEncoding.DecodeString(requestBase64) | |||
if err != nil { | |||
return &DNSRequest{ | |||
errcode: 400, | |||
errtext: fmt.Sprintf("Invalid argument value: \"dns\" = %q", requestBase64), | |||
} | |||
} | |||
if len(requestBinary) == 0 && (r.Header.Get("Content-Type") == "application/dns-message" || r.Header.Get("Content-Type") == "application/dns-udpwireformat") { | |||
requestBinary, err = ioutil.ReadAll(r.Body) | |||
if err != nil { | |||
return &DNSRequest{ | |||
errcode: 400, | |||
errtext: fmt.Sprintf("Failed to read request body (%s)", err.Error()), | |||
} | |||
} | |||
} | |||
if len(requestBinary) == 0 { | |||
return &DNSRequest{ | |||
errcode: 400, | |||
errtext: fmt.Sprintf("Invalid argument value: \"dns\""), | |||
} | |||
} | |||
if server.patchDNSCryptProxyReqID(w, r, requestBinary) { | |||
return &DNSRequest{ | |||
errcode: 444, | |||
} | |||
} | |||
msg := new(dns.Msg) | |||
err = msg.Unpack(requestBinary) | |||
if err != nil { | |||
return &DNSRequest{ | |||
errcode: 400, | |||
errtext: fmt.Sprintf("DNS packet parse failure (%s)", err.Error()), | |||
} | |||
} | |||
if server.Verbose && len(msg.Question) > 0 { | |||
question := &msg.Question[0] | |||
questionName := question.Name | |||
questionClass := "" | |||
if qclass, ok := dns.ClassToString[question.Qclass]; ok { | |||
questionClass = qclass | |||
} else { | |||
questionClass = strconv.FormatUint(uint64(question.Qclass), 10) | |||
} | |||
questionType := "" | |||
if qtype, ok := dns.TypeToString[question.Qtype]; ok { | |||
questionType = qtype | |||
} else { | |||
questionType = strconv.FormatUint(uint64(question.Qtype), 10) | |||
} | |||
var clientip net.IP = nil | |||
if server.LogGuessedIP { | |||
clientip = server.findClientIP(r) | |||
} | |||
if clientip != nil { | |||
fmt.Printf("%s - - [%s] \"%s %s %s\"\n", clientip, time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionClass, questionType) | |||
} else { | |||
fmt.Printf("%s - - [%s] \"%s %s %s\"\n", r.RemoteAddr, time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionClass, questionType) | |||
} | |||
} | |||
transactionID := msg.Id | |||
msg.Id = dns.Id() | |||
opt := msg.IsEdns0() | |||
if opt == nil { | |||
opt = new(dns.OPT) | |||
opt.Hdr.Name = "." | |||
opt.Hdr.Rrtype = dns.TypeOPT | |||
opt.SetUDPSize(dns.DefaultMsgSize) | |||
opt.SetDo(false) | |||
msg.Extra = append([]dns.RR{opt}, msg.Extra...) | |||
} | |||
var edns0Subnet *dns.EDNS0_SUBNET | |||
for _, option := range opt.Option { | |||
if option.Option() == dns.EDNS0SUBNET { | |||
edns0Subnet = option.(*dns.EDNS0_SUBNET) | |||
break | |||
} | |||
} | |||
isTailored := edns0Subnet == nil | |||
if edns0Subnet == nil { | |||
ednsClientFamily := uint16(0) | |||
ednsClientAddress := server.findClientIP(r) | |||
ednsClientNetmask := uint8(255) | |||
if ednsClientAddress != nil { | |||
if ipv4 := ednsClientAddress.To4(); ipv4 != nil { | |||
ednsClientFamily = 1 | |||
ednsClientAddress = ipv4 | |||
ednsClientNetmask = 24 | |||
} else { | |||
ednsClientFamily = 2 | |||
ednsClientNetmask = 56 | |||
} | |||
edns0Subnet = new(dns.EDNS0_SUBNET) | |||
edns0Subnet.Code = dns.EDNS0SUBNET | |||
edns0Subnet.Family = ednsClientFamily | |||
edns0Subnet.SourceNetmask = ednsClientNetmask | |||
edns0Subnet.SourceScope = 0 | |||
edns0Subnet.Address = ednsClientAddress | |||
opt.Option = append(opt.Option, edns0Subnet) | |||
} | |||
} | |||
return &DNSRequest{ | |||
request: msg, | |||
transactionID: transactionID, | |||
isTailored: isTailored, | |||
} | |||
} | |||
func (server *Server) generateResponseIETF(ctx context.Context, w http.ResponseWriter, r *http.Request, req *DNSRequest) { | |||
respJSON := Marshal(req.response) | |||
req.response.Id = req.transactionID | |||
respBytes, err := req.response.Pack() | |||
if err != nil { | |||
log.Printf("DNS packet construct failure with upstream %s: %v\n", req.currentUpstream, err) | |||
formatError(w, fmt.Sprintf("DNS packet construct failure (%s)", err.Error()), 500) | |||
return | |||
} | |||
w.Header().Set("Content-Type", "application/dns-message") | |||
now := time.Now().UTC().Format(http.TimeFormat) | |||
w.Header().Set("Date", now) | |||
w.Header().Set("Last-Modified", now) | |||
w.Header().Set("Vary", "Accept") | |||
_ = server.patchFirefoxContentType(w, r, req) | |||
if respJSON.HaveTTL { | |||
if req.isTailored { | |||
w.Header().Set("Cache-Control", "private, max-age="+strconv.FormatUint(uint64(respJSON.LeastTTL), 10)) | |||
} else { | |||
w.Header().Set("Cache-Control", "public, max-age="+strconv.FormatUint(uint64(respJSON.LeastTTL), 10)) | |||
} | |||
w.Header().Set("Expires", respJSON.EarliestExpires.Format(http.TimeFormat)) | |||
} | |||
if respJSON.Status == dns.RcodeServerFailure { | |||
log.Printf("received server failure from upstream %s: %v\n", req.currentUpstream, req.response) | |||
w.WriteHeader(503) | |||
} | |||
_, err = w.Write(respBytes) | |||
if err != nil { | |||
log.Printf("failed to write to client: %v\n", err) | |||
} | |||
} | |||
// Workaround a bug causing DNSCrypt-Proxy to expect a response with TransactionID = 0xcafe | |||
func (server *Server) patchDNSCryptProxyReqID(w http.ResponseWriter, r *http.Request, requestBinary []byte) bool { | |||
if strings.Contains(r.UserAgent(), "dnscrypt-proxy") && bytes.Equal(requestBinary, []byte("\xca\xfe\x01\x00\x00\x01\x00\x00\x00\x00\x00\x01\x00\x00\x02\x00\x01\x00\x00\x29\x10\x00\x00\x00\x80\x00\x00\x00")) { | |||
log.Println("DNSCrypt-Proxy detected. Patching response.") | |||
w.Header().Set("Content-Type", "application/dns-message") | |||
w.Header().Set("Vary", "Accept, User-Agent") | |||
now := time.Now().UTC().Format(http.TimeFormat) | |||
w.Header().Set("Date", now) | |||
w.Write([]byte("\xca\xfe\x81\x05\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x02\x00\x01\x00\x00\x10\x00\x01\x00\x00\x00\x00\x00\xa8\xa7\r\nWorkaround a bug causing DNSCrypt-Proxy to expect a response with TransactionID = 0xcafe\r\nRefer to https://github.com/jedisct1/dnscrypt-proxy/issues/526 for details.")) | |||
return true | |||
} | |||
return false | |||
} | |||
// Workaround a bug causing Firefox 61-62 to reject responses with Content-Type = application/dns-message | |||
func (server *Server) patchFirefoxContentType(w http.ResponseWriter, r *http.Request, req *DNSRequest) bool { | |||
if strings.Contains(r.UserAgent(), "Firefox") && strings.Contains(r.Header.Get("Accept"), "application/dns-udpwireformat") && !strings.Contains(r.Header.Get("Accept"), "application/dns-message") { | |||
log.Println("Firefox 61-62 detected. Patching response.") | |||
w.Header().Set("Content-Type", "application/dns-udpwireformat") | |||
w.Header().Set("Vary", "Accept, User-Agent") | |||
req.isTailored = true | |||
return true | |||
} | |||
return false | |||
} |
@ -0,0 +1,17 @@ | |||
-----BEGIN CERTIFICATE----- | |||
MIICpDCCAYwCCQCk39ob2iYrRzANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls | |||
b2NhbGhvc3QwHhcNMjAwMzA1MDg1NTM3WhcNNDUwMjI3MDg1NTM3WjAUMRIwEAYD | |||
VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9 | |||
Z1nEL6UGgDin67kAkUaftQMP7hi4OoAym9sMllN2jaYi0yXhNSWAXXegzqAHMFwa | |||
LyEQEYFGUeZB0a7f+nJSmj6ZYCZvWasXGEHvzCfTsa8APN2s/ne6gEHPf3pzvbWq | |||
Z8Ao3eezQLI0y2Vd3aBcxG/Be/UnnKSRQP4vrObJGvkdIZUpdH6/xoYHtF5HM3Df | |||
kKtM2wr3OYc1gjeKXZipxd8G7GfB9ycb+RqQ/TJTQ5EOeCGbBc31o2HpaapcBbFC | |||
LsgbyUJ9sXgPmXEd3Dm6EM9SHEWlkm+P04VHL10qUjKKRVWkvfuMmSgujY3JRf7F | |||
ak0MxeLn2c1WbFH+TEpxAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAARjQkprUUbU | |||
3uua+oVQDZnTN7rF9nHoNL5qd3V04lhZo6LA+7aHoMhQD9JNZtwYOCPDNzee3jUa | |||
ogXyDT2wdI88vhXzeGYLnZrD7fyAV12XHPzakWKEN/faoYg7wOmBHeTsKv63YrUh | |||
+UpeCw68u2L1xpK9dFoL+ApiyRRn+BcaKwlWnEKcphi9nPj0nE4qaNqStKrz9xhd | |||
8buVPuTpQZVXtJ1lfpvBkFoLaucm3bShrUhb7pBIOgWWx8K+qtETxhsnSA7dUvKA | |||
7cwmOhq3lyp1mMqAZd7E5tg8DrFx0I4DHptCp54aJM9W1LHcnEWFJi11jb2pzLzw | |||
westleTT8ps= | |||
-----END CERTIFICATE----- |
@ -0,0 +1,27 @@ | |||
-----BEGIN RSA PRIVATE KEY----- | |||
MIIEpQIBAAKCAQEAvWdZxC+lBoA4p+u5AJFGn7UDD+4YuDqAMpvbDJZTdo2mItMl | |||
4TUlgF13oM6gBzBcGi8hEBGBRlHmQdGu3/pyUpo+mWAmb1mrFxhB78wn07GvADzd | |||
rP53uoBBz396c721qmfAKN3ns0CyNMtlXd2gXMRvwXv1J5ykkUD+L6zmyRr5HSGV | |||
KXR+v8aGB7ReRzNw35CrTNsK9zmHNYI3il2YqcXfBuxnwfcnG/kakP0yU0ORDngh | |||
mwXN9aNh6WmqXAWxQi7IG8lCfbF4D5lxHdw5uhDPUhxFpZJvj9OFRy9dKlIyikVV | |||
pL37jJkoLo2NyUX+xWpNDMXi59nNVmxR/kxKcQIDAQABAoIBAQCEzLU9AGcGAts2 | |||
qemiQzowept2DOxaJ/KBCZRx4+kLY9AL9N5HZJsxwNdC8f10bOz3EvpsqMlqg7wd | |||
hCbINnL4BdxEcA0i3809OS3qM8vs+1WHpiWLyTQrmQgLtAcopeh9XZd3T/fIUGFi | |||
8QXW5bEtujHdiMtghc1BZz+SL/n1IHV0D04RNPA2KMRom2xROGbIqv9x0BpS4vev | |||
Cwaf2ofM8Z990LJ9ot9D22MUxxGY04ViY07uZteliLv/z/qzKF5HrpmRdYZ2e+/q | |||
026Ua2PjiQ40yFAz2ektOGm3LaXiqOJxUQPYShrLWhBSs4K4hcsEkpcFqEoajSxV | |||
FrD9Er0dAoGBAPB547skkkBj/Zrdgsd7c+FxD4k5852unLyzFs8BD2a75oQJ02iw | |||
JcCACtQX1Nd3EwzjSgOiVSmeszi6J8mOgNzTrokiEKXcmnJd0kHIWRA0JqqrtCJF | |||
exMzaP5u4yJsCijPFh06U3arlU2XK5FOcvZiyPFQoeQcQCEza8efhidPAoGBAMmh | |||
cBpL0jrR/81shlfpJKvMsQiCRhucUUETHAyUX9zzI/NaBf3Qca2GmziMz9PFexib | |||
YRjPAJlOfubmzI7W2aSAs9x/3OFmRea4XHkJHvJUgoyNi6BisT3NcLu9XWNk7rtc | |||
VUJhevE9q2+ZQtudiQt7FZMnxgW+US5k9Xbx5gI/AoGBANPc33lCSCO4tHcbTxwG | |||
tNpa7LAewXYbn3VUZvTrXzFIvFd5/KrP/gKyDFg9wsQt4TfKi6vV+ifX7Ng+kc0u | |||
4nMrgCrLO1WVnPDDnflc1LLE74gQDHzhMASDl64J7cym2PCJOld3yo7Tro+Ubsrv | |||
DbPq5lRMkMTS6uEVV5ChB+VbAoGARdtq7ZFravmq+M8q1HZwQB2REHOiOpq0BCnM | |||
xAb8F58dy4hbHw8C8635RWRz9Nksxt++ikvd1z+8897u7GY/zaDRsAmUy3sVqNQj | |||
JcQlNqxU9sFrqMvIwLLW5hS7sF4d4EgjOfZwE/jb1rRw14oDGzkvxmY3U3IWyk4s | |||
RWOV3x0CgYEAkAXU8kUxANWRIJ+8IRXh6gYU7m+HeARjCwC5PUMGCsalgFB9vPIq | |||
9rzZUPUPczEEeAFKHK+tgXAQD0fvZ9fUseJMcBlkxeBDeKaEs8TiFsA1LxUEh5r2 | |||
SU2GxXWusut4SY0vE72SiV9RHJ4zPlPWOBHcwVuTwiFrs0PAHsOSc2w= | |||
-----END RSA PRIVATE KEY----- |