Merge branch 'ietf'

This commit is contained in:
Star Brilliant
2018-03-21 02:07:57 +08:00
19 changed files with 1174 additions and 580 deletions

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (C) 2017 Star Brilliant <m13253@hotmail.com> Copyright (C) 2017-2018 Star Brilliant <m13253@hotmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in this software and associated documentation files (the "Software"), to deal in

View File

@@ -1,7 +1,8 @@
DNS-over-HTTPS DNS-over-HTTPS
============== ==============
Client and server software to query DNS over HTTPS, using [Google DNS-over-HTTPS protocol](https://developers.google.com/speed/public-dns/docs/dns-over-https). Client and server software to query DNS over HTTPS, using [Google DNS-over-HTTPS protocol](https://developers.google.com/speed/public-dns/docs/dns-over-https)
and [draft-ietf-doh-dns-over-https](https://github.com/dohwg/draft-ietf-doh-dns-over-https).
## Easy start ## Easy start
@@ -71,11 +72,22 @@ Client Subnet during your configuring `unbound` or `bind`.
## Protocol compatibility ## Protocol compatibility
DNS-over-HTTPS use a protocol compatible to [Google DNS-over-HTTPS](https://developers.google.com/speed/public-dns/docs/dns-over-https), ### Google DNS-over-HTTPS
DNS-over-HTTPS uses a protocol compatible to [Google DNS-over-HTTPS](https://developers.google.com/speed/public-dns/docs/dns-over-https),
except for absolute expire time is preferred to relative TTL value. Refer to except for absolute expire time is preferred to relative TTL value. Refer to
[json-dns/response.go](json-dns/response.go) for a complete description of the [json-dns/response.go](json-dns/response.go) for a complete description of the
API. API.
### IETF DNS-over-HTTPS (Draft)
DNS-over-HTTPS uses a protocol compatible to [draft-ietf-doh-dns-over-https
](https://github.com/dohwg/draft-ietf-doh-dns-over-https).
This protocol is in draft stage. Any incompatibility may be introduced before
it is finished.
### Supported features
Currently supported features are: Currently supported features are:
- [X] IPv4 / IPv6 - [X] IPv4 / IPv6

View File

@@ -1,6 +1,6 @@
/* /*
DNS-over-HTTPS DNS-over-HTTPS
Copyright (C) 2017 Star Brilliant <m13253@hotmail.com> Copyright (C) 2017-2018 Star Brilliant <m13253@hotmail.com>
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -25,20 +25,16 @@ package main
import ( import (
"context" "context"
"encoding/json"
"fmt"
"math/rand"
"io/ioutil"
"log" "log"
"math/rand"
"net" "net"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"net/url"
"strconv"
"strings"
"time" "time"
"github.com/miekg/dns"
"../json-dns" "../json-dns"
"github.com/miekg/dns"
"golang.org/x/net/http2"
) )
type Client struct { type Client struct {
@@ -51,16 +47,16 @@ type Client struct {
} }
func NewClient(conf *config) (c *Client, err error) { func NewClient(conf *config) (c *Client, err error) {
c = &Client { c = &Client{
conf: conf, conf: conf,
} }
c.udpServer = &dns.Server { c.udpServer = &dns.Server{
Addr: conf.Listen, Addr: conf.Listen,
Net: "udp", Net: "udp",
Handler: dns.HandlerFunc(c.udpHandlerFunc), Handler: dns.HandlerFunc(c.udpHandlerFunc),
UDPSize: 4096, UDPSize: 4096,
} }
c.tcpServer = &dns.Server { c.tcpServer = &dns.Server{
Addr: conf.Listen, Addr: conf.Listen,
Net: "tcp", Net: "tcp",
Handler: dns.HandlerFunc(c.tcpHandlerFunc), Handler: dns.HandlerFunc(c.tcpHandlerFunc),
@@ -71,35 +67,47 @@ func NewClient(conf *config) (c *Client, err error) {
for i, bootstrap := range conf.Bootstrap { for i, bootstrap := range conf.Bootstrap {
bootstrapAddr, err := net.ResolveUDPAddr("udp", bootstrap) bootstrapAddr, err := net.ResolveUDPAddr("udp", bootstrap)
if err != nil { if err != nil {
bootstrapAddr, err = net.ResolveUDPAddr("udp", "[" + bootstrap + "]:53") bootstrapAddr, err = net.ResolveUDPAddr("udp", "["+bootstrap+"]:53")
}
if err != nil {
return nil, err
} }
if err != nil { return nil, err }
c.bootstrap[i] = bootstrapAddr.String() c.bootstrap[i] = bootstrapAddr.String()
} }
bootResolver = &net.Resolver { bootResolver = &net.Resolver{
PreferGo: true, PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) { Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
var d net.Dialer var d net.Dialer
num_servers := len(c.bootstrap) numServers := len(c.bootstrap)
bootstrap := c.bootstrap[rand.Intn(num_servers)] bootstrap := c.bootstrap[rand.Intn(numServers)]
conn, err := d.DialContext(ctx, network, bootstrap) conn, err := d.DialContext(ctx, network, bootstrap)
return conn, err return conn, err
}, },
} }
} }
c.httpTransport = new(http.Transport) c.httpTransport = new(http.Transport)
*c.httpTransport = *http.DefaultTransport.(*http.Transport) c.httpTransport = &http.Transport{
c.httpTransport.DialContext = (&net.Dialer { DialContext: (&net.Dialer{
Timeout: time.Duration(conf.Timeout) * time.Second, Timeout: time.Duration(conf.Timeout) * time.Second,
KeepAlive: 30 * time.Second, KeepAlive: 30 * time.Second,
DualStack: true, DualStack: true,
Resolver: bootResolver, Resolver: bootResolver,
}).DialContext }).DialContext,
c.httpTransport.ResponseHeaderTimeout = time.Duration(conf.Timeout) * time.Second ExpectContinueTimeout: 1 * time.Second,
IdleConnTimeout: 90 * time.Second,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
Proxy: http.ProxyFromEnvironment,
ResponseHeaderTimeout: time.Duration(conf.Timeout) * time.Second,
TLSHandshakeTimeout: time.Duration(conf.Timeout) * time.Second,
}
http2.ConfigureTransport(c.httpTransport)
// Most CDNs require Cookie support to prevent DDoS attack // Most CDNs require Cookie support to prevent DDoS attack
cookieJar, err := cookiejar.New(nil) cookieJar, err := cookiejar.New(nil)
if err != nil { return nil, err } if err != nil {
c.httpClient = &http.Client { return nil, err
}
c.httpClient = &http.Client{
Transport: c.httpTransport, Transport: c.httpTransport,
Jar: cookieJar, Jar: cookieJar,
} }
@@ -114,14 +122,14 @@ func (c *Client) Start() error {
log.Println(err) log.Println(err)
} }
result <- err result <- err
} () }()
go func() { go func() {
err := c.tcpServer.ListenAndServe() err := c.tcpServer.ListenAndServe()
if err != nil { if err != nil {
log.Println(err) log.Println(err)
} }
result <- err result <- err
} () }()
err := <-result err := <-result
if err != nil { if err != nil {
return err return err
@@ -136,110 +144,21 @@ func (c *Client) handlerFunc(w dns.ResponseWriter, r *dns.Msg, isTCP bool) {
return return
} }
reply := jsonDNS.PrepareReply(r) if len(c.conf.UpstreamIETF) == 0 {
c.handlerFuncGoogle(w, r, isTCP)
if len(r.Question) != 1 {
log.Println("Number of questions is not 1")
reply.Rcode = dns.RcodeFormatError
w.WriteMsg(reply)
return return
} }
question := r.Question[0] if len(c.conf.UpstreamGoogle) == 0 {
questionName := strings.ToLower(question.Name) c.handlerFuncIETF(w, r, isTCP)
questionType := "" return
if qtype, ok := dns.TypeToString[question.Qtype]; ok { }
questionType = qtype numServers := len(c.conf.UpstreamGoogle) + len(c.conf.UpstreamIETF)
random := rand.Intn(numServers)
if random < len(c.conf.UpstreamGoogle) {
c.handlerFuncGoogle(w, r, isTCP)
} else { } else {
questionType = strconv.Itoa(int(question.Qtype)) c.handlerFuncIETF(w, r, isTCP)
} }
if c.conf.Verbose {
fmt.Printf("%s - - [%s] \"%s IN %s\"\n", w.RemoteAddr(), time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionType)
}
num_servers := len(c.conf.Upstream)
upstream := c.conf.Upstream[rand.Intn(num_servers)]
requestURL := fmt.Sprintf("%s?name=%s&type=%s", upstream, url.QueryEscape(questionName), url.QueryEscape(questionType))
if r.CheckingDisabled {
requestURL += "&cd=1"
}
udpSize := uint16(512)
if opt := r.IsEdns0(); opt != nil {
udpSize = opt.UDPSize()
}
ednsClientAddress, ednsClientNetmask := c.findClientIP(w, r)
if ednsClientAddress != nil {
requestURL += fmt.Sprintf("&edns_client_subnet=%s/%d", ednsClientAddress.String(), ednsClientNetmask)
}
req, err := http.NewRequest("GET", requestURL, nil)
if err != nil {
log.Println(err)
reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply)
return
}
req.Header.Set("User-Agent", "DNS-over-HTTPS/1.0 (+https://github.com/m13253/dns-over-https)")
resp, err := c.httpClient.Do(req)
if err != nil {
log.Println(err)
reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply)
c.httpTransport.CloseIdleConnections()
return
}
if resp.StatusCode != 200 {
log.Printf("HTTP error: %s\n", resp.Status)
reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply)
contentType := resp.Header.Get("Content-Type")
if contentType != "application/json" && !strings.HasPrefix(contentType, "application/json;") {
return
}
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println(err)
reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply)
return
}
var respJson jsonDNS.Response
err = json.Unmarshal(body, &respJson)
if err != nil {
log.Println(err)
reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply)
return
}
if respJson.Status != dns.RcodeSuccess && respJson.Comment != "" {
log.Printf("DNS error: %s\n", respJson.Comment)
}
fullReply := jsonDNS.Unmarshal(reply, &respJson, udpSize, ednsClientNetmask)
buf, err := fullReply.Pack()
if err != nil {
log.Println(err)
reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply)
return
}
if !isTCP && len(buf) > int(udpSize) {
fullReply.Truncated = true
buf, err = fullReply.Pack()
if err != nil {
log.Println(err)
return
}
buf = buf[:udpSize]
}
w.Write(buf)
} }
func (c *Client) udpHandlerFunc(w dns.ResponseWriter, r *dns.Msg) { func (c *Client) udpHandlerFunc(w dns.ResponseWriter, r *dns.Msg) {
@@ -251,8 +170,8 @@ func (c *Client) tcpHandlerFunc(w dns.ResponseWriter, r *dns.Msg) {
} }
var ( var (
ipv4Mask24 net.IPMask = net.IPMask { 255, 255, 255, 0 } ipv4Mask24 = net.IPMask{255, 255, 255, 0}
ipv6Mask48 net.IPMask = net.CIDRMask(48, 128) ipv6Mask48 = net.CIDRMask(48, 128)
) )
func (c *Client) findClientIP(w dns.ResponseWriter, r *dns.Msg) (ednsClientAddress net.IP, ednsClientNetmask uint8) { func (c *Client) findClientIP(w dns.ResponseWriter, r *dns.Msg) (ednsClientAddress net.IP, ednsClientNetmask uint8) {

View File

@@ -1,6 +1,6 @@
/* /*
DNS-over-HTTPS DNS-over-HTTPS
Copyright (C) 2017 Star Brilliant <m13253@hotmail.com> Copyright (C) 2017-2018 Star Brilliant <m13253@hotmail.com>
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -25,12 +25,14 @@ package main
import ( import (
"fmt" "fmt"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
) )
type config struct { type config struct {
Listen string `toml:"listen"` Listen string `toml:"listen"`
Upstream []string `toml:"upstream"` UpstreamGoogle []string `toml:"upstream_google"`
UpstreamIETF []string `toml:"upstream_ietf"`
Bootstrap []string `toml:"bootstrap"` Bootstrap []string `toml:"bootstrap"`
Timeout uint `toml:"timeout"` Timeout uint `toml:"timeout"`
NoECS bool `toml:"no_ecs"` NoECS bool `toml:"no_ecs"`
@@ -38,20 +40,20 @@ type config struct {
} }
func loadConfig(path string) (*config, error) { func loadConfig(path string) (*config, error) {
conf := &config {} conf := &config{}
metaData, err := toml.DecodeFile(path, conf) metaData, err := toml.DecodeFile(path, conf)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, key := range metaData.Undecoded() { for _, key := range metaData.Undecoded() {
return nil, &configError { fmt.Sprintf("unknown option %q", key.String()) } return nil, &configError{fmt.Sprintf("unknown option %q", key.String())}
} }
if conf.Listen == "" { if conf.Listen == "" {
conf.Listen = "127.0.0.1:53" conf.Listen = "127.0.0.1:53"
} }
if len(conf.Upstream) == 0 { if len(conf.UpstreamGoogle) == 0 && len(conf.UpstreamIETF) == 0 {
conf.Upstream = []string { "https://dns.google.com/resolve" } conf.UpstreamGoogle = []string{"https://dns.google.com/resolve"}
} }
if conf.Timeout == 0 { if conf.Timeout == 0 {
conf.Timeout = 10 conf.Timeout = 10

View File

@@ -3,9 +3,12 @@ listen = "127.0.0.1:53"
# HTTP path for upstream resolver # HTTP path for upstream resolver
# If multiple servers are specified, a random one will be chosen each time. # If multiple servers are specified, a random one will be chosen each time.
upstream = [ upstream_google = [
"https://dns.google.com/resolve", "https://dns.google.com/resolve",
] ]
upstream_ietf = [
"https://dns.google.com/experimental",
]
# Bootstrap DNS server to resolve the address of the upstream resolver # Bootstrap DNS server to resolve the address of the upstream resolver
# If multiple servers are specified, a random one will be chosen each time. # If multiple servers are specified, a random one will be chosen each time.

149
doh-client/google.go Normal file
View File

@@ -0,0 +1,149 @@
/*
DNS-over-HTTPS
Copyright (C) 2017-2018 Star Brilliant <m13253@hotmail.com>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"math/rand"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"../json-dns"
"github.com/miekg/dns"
)
func (c *Client) handlerFuncGoogle(w dns.ResponseWriter, r *dns.Msg, isTCP bool) {
reply := jsonDNS.PrepareReply(r)
if len(r.Question) != 1 {
log.Println("Number of questions is not 1")
reply.Rcode = dns.RcodeFormatError
w.WriteMsg(reply)
return
}
question := r.Question[0]
// knot-resolver scrambles capitalization, I think it is unfriendly to cache
questionName := strings.ToLower(question.Name)
questionType := ""
if qtype, ok := dns.TypeToString[question.Qtype]; ok {
questionType = qtype
} else {
questionType = strconv.Itoa(int(question.Qtype))
}
if c.conf.Verbose {
fmt.Printf("%s - - [%s] \"%s IN %s\"\n", w.RemoteAddr(), time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionType)
}
numServers := len(c.conf.UpstreamGoogle)
upstream := c.conf.UpstreamGoogle[rand.Intn(numServers)]
requestURL := fmt.Sprintf("%s?name=%s&type=%s", upstream, url.QueryEscape(questionName), url.QueryEscape(questionType))
if r.CheckingDisabled {
requestURL += "&cd=1"
}
udpSize := uint16(512)
if opt := r.IsEdns0(); opt != nil {
udpSize = opt.UDPSize()
}
ednsClientAddress, ednsClientNetmask := c.findClientIP(w, r)
if ednsClientAddress != nil {
requestURL += fmt.Sprintf("&edns_client_subnet=%s/%d", ednsClientAddress.String(), ednsClientNetmask)
}
req, err := http.NewRequest("GET", requestURL, nil)
if err != nil {
log.Println(err)
reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply)
return
}
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "DNS-over-HTTPS/1.0 (+https://github.com/m13253/dns-over-https)")
resp, err := c.httpClient.Do(req)
if err != nil {
log.Println(err)
reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply)
c.httpTransport.CloseIdleConnections()
return
}
if resp.StatusCode != 200 {
log.Printf("HTTP error: %s\n", resp.Status)
reply.Rcode = dns.RcodeServerFailure
contentType := resp.Header.Get("Content-Type")
if contentType != "application/json" && !strings.HasPrefix(contentType, "application/json;") {
w.WriteMsg(reply)
return
}
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println(err)
reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply)
return
}
var respJSON jsonDNS.Response
err = json.Unmarshal(body, &respJSON)
if err != nil {
log.Println(err)
reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply)
return
}
if respJSON.Status != dns.RcodeSuccess && respJSON.Comment != "" {
log.Printf("DNS error: %s\n", respJSON.Comment)
}
fullReply := jsonDNS.Unmarshal(reply, &respJSON, udpSize, ednsClientNetmask)
buf, err := fullReply.Pack()
if err != nil {
log.Println(err)
reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply)
return
}
if !isTCP && len(buf) > int(udpSize) {
fullReply.Truncated = true
buf, err = fullReply.Pack()
if err != nil {
log.Println(err)
return
}
buf = buf[:udpSize]
}
w.Write(buf)
}

236
doh-client/ietf.go Normal file
View File

@@ -0,0 +1,236 @@
/*
DNS-over-HTTPS
Copyright (C) 2017-2018 Star Brilliant <m13253@hotmail.com>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
package main
import (
"bytes"
"encoding/base64"
"fmt"
"io/ioutil"
"log"
"math/rand"
"net/http"
"strconv"
"strings"
"time"
"../json-dns"
"github.com/miekg/dns"
)
func (c *Client) handlerFuncIETF(w dns.ResponseWriter, r *dns.Msg, isTCP bool) {
reply := jsonDNS.PrepareReply(r)
if len(r.Question) != 1 {
log.Println("Number of questions is not 1")
reply.Rcode = dns.RcodeFormatError
w.WriteMsg(reply)
return
}
question := r.Question[0]
// knot-resolver scrambles capitalization, I think it is unfriendly to cache
questionName := strings.ToLower(question.Name)
questionType := ""
if qtype, ok := dns.TypeToString[question.Qtype]; ok {
questionType = qtype
} else {
questionType = strconv.Itoa(int(question.Qtype))
}
if c.conf.Verbose {
fmt.Printf("%s - - [%s] \"%s IN %s\"\n", w.RemoteAddr(), time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionType)
}
requestID := r.Id
r.Id = 0
question.Name = questionName
opt := r.IsEdns0()
udpSize := uint16(512)
if opt == nil {
opt = new(dns.OPT)
opt.Hdr.Name = "."
opt.Hdr.Rrtype = dns.TypeOPT
opt.SetUDPSize(4096)
opt.SetDo(false)
r.Extra = append(r.Extra, opt)
} else {
udpSize = opt.UDPSize()
}
var edns0Subnet *dns.EDNS0_SUBNET
for _, option := range opt.Option {
if option.Option() == dns.EDNS0SUBNET {
edns0Subnet = option.(*dns.EDNS0_SUBNET)
break
}
}
if edns0Subnet == nil {
ednsClientFamily := uint16(0)
ednsClientAddress, ednsClientNetmask := c.findClientIP(w, r)
if ednsClientAddress != nil {
if ipv4 := ednsClientAddress.To4(); ipv4 != nil {
ednsClientFamily = 1
ednsClientAddress = ipv4
ednsClientNetmask = 24
} else {
ednsClientFamily = 2
ednsClientNetmask = 48
}
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)
}
}
requestBinary, err := r.Pack()
if err != nil {
log.Println(err)
reply.Rcode = dns.RcodeFormatError
w.WriteMsg(reply)
return
}
requestBase64 := base64.RawURLEncoding.EncodeToString(requestBinary)
numServers := len(c.conf.UpstreamIETF)
upstream := c.conf.UpstreamIETF[rand.Intn(numServers)]
requestURL := fmt.Sprintf("%s?ct=application/dns-udpwireformat&dns=%s", upstream, requestBase64)
var req *http.Request
if len(requestURL) < 2048 {
req, err = http.NewRequest("GET", requestURL, nil)
if err != nil {
log.Println(err)
reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply)
return
}
} else {
req, err = http.NewRequest("POST", upstream, bytes.NewReader(requestBinary))
if err != nil {
log.Println(err)
reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply)
return
}
req.Header.Set("Content-Type", "application/dns-udpwireformat")
}
req.Header.Set("Accept", "application/dns-udpwireformat")
req.Header.Set("User-Agent", "DNS-over-HTTPS/1.0 (+https://github.com/m13253/dns-over-https)")
resp, err := c.httpClient.Do(req)
if err != nil {
log.Println(err)
reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply)
c.httpTransport.CloseIdleConnections()
return
}
if resp.StatusCode != 200 {
log.Printf("HTTP error: %s\n", resp.Status)
reply.Rcode = dns.RcodeServerFailure
contentType := resp.Header.Get("Content-Type")
if contentType != "application/dns-udpwireformat" && !strings.HasPrefix(contentType, "application/dns-udpwireformat;") {
w.WriteMsg(reply)
return
}
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println(err)
reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply)
return
}
lastModified := resp.Header.Get("Last-Modified")
if lastModified == "" {
lastModified = resp.Header.Get("Date")
}
now := time.Now()
lastModifiedDate, err := time.Parse(http.TimeFormat, lastModified)
if err != nil {
log.Println(err)
lastModifiedDate = now
}
timeDelta := now.Sub(lastModifiedDate)
if timeDelta < 0 {
timeDelta = 0
}
fullReply := new(dns.Msg)
err = fullReply.Unpack(body)
if err != nil {
log.Println(err)
reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply)
return
}
fullReply.Id = requestID
for _, rr := range fullReply.Answer {
_ = fixRecordTTL(rr, timeDelta)
}
for _, rr := range fullReply.Ns {
_ = fixRecordTTL(rr, timeDelta)
}
for _, rr := range fullReply.Extra {
if rr.Header().Rrtype == dns.TypeOPT {
continue
}
_ = fixRecordTTL(rr, timeDelta)
}
buf, err := fullReply.Pack()
if err != nil {
log.Println(err)
reply.Rcode = dns.RcodeServerFailure
w.WriteMsg(reply)
return
}
if !isTCP && len(buf) > int(udpSize) {
fullReply.Truncated = true
buf, err = fullReply.Pack()
if err != nil {
log.Println(err)
return
}
buf = buf[:udpSize]
}
w.Write(buf)
}
func fixRecordTTL(rr dns.RR, delta time.Duration) dns.RR {
rrHeader := rr.Header()
oldTTL := time.Duration(rrHeader.Ttl) * time.Second
newTTL := oldTTL - delta
if newTTL >= 0 {
rrHeader.Ttl = uint32(newTTL / time.Second)
} else {
rrHeader.Ttl = 0
}
return rr
}

View File

@@ -1,6 +1,6 @@
/* /*
DNS-over-HTTPS DNS-over-HTTPS
Copyright (C) 2017 Star Brilliant <m13253@hotmail.com> Copyright (C) 2017-2018 Star Brilliant <m13253@hotmail.com>
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -1,6 +1,6 @@
/* /*
DNS-over-HTTPS DNS-over-HTTPS
Copyright (C) 2017 Star Brilliant <m13253@hotmail.com> Copyright (C) 2017-2018 Star Brilliant <m13253@hotmail.com>
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -25,6 +25,7 @@ package main
import ( import (
"fmt" "fmt"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
) )
@@ -41,23 +42,23 @@ type config struct {
} }
func loadConfig(path string) (*config, error) { func loadConfig(path string) (*config, error) {
conf := &config {} conf := &config{}
metaData, err := toml.DecodeFile(path, conf) metaData, err := toml.DecodeFile(path, conf)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, key := range metaData.Undecoded() { for _, key := range metaData.Undecoded() {
return nil, &configError { fmt.Sprintf("unknown option %q", key.String()) } return nil, &configError{fmt.Sprintf("unknown option %q", key.String())}
} }
if conf.Listen == "" { if conf.Listen == "" {
conf.Listen = "127.0.0.1:8053" conf.Listen = "127.0.0.1:8053"
} }
if conf.Path == "" { if conf.Path == "" {
conf.Path = "/resolve" conf.Path = "/dns-query"
} }
if len(conf.Upstream) == 0 { if len(conf.Upstream) == 0 {
conf.Upstream = []string { "8.8.8.8:53", "8.8.4.4:53" } conf.Upstream = []string{"8.8.8.8:53", "8.8.4.4:53"}
} }
if conf.Timeout == 0 { if conf.Timeout == 0 {
conf.Timeout = 10 conf.Timeout = 10
@@ -67,7 +68,7 @@ func loadConfig(path string) (*config, error) {
} }
if (conf.Cert != "") != (conf.Key != "") { if (conf.Cert != "") != (conf.Key != "") {
return nil, &configError { "You must specify both -cert and -key to enable TLS" } return nil, &configError{"You must specify both -cert and -key to enable TLS"}
} }
return conf, nil return conf, nil

View File

@@ -8,7 +8,7 @@ cert = ""
key = "" key = ""
# HTTP path for resolve application # HTTP path for resolve application
path = "/resolve" path = "/dns-query"
# Upstream DNS resolver # Upstream DNS resolver
# If multiple servers are specified, a random one will be chosen each time. # If multiple servers are specified, a random one will be chosen each time.

193
doh-server/google.go Normal file
View File

@@ -0,0 +1,193 @@
/*
DNS-over-HTTPS
Copyright (C) 2017-2018 Star Brilliant <m13253@hotmail.com>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
package main
import (
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"strconv"
"strings"
"../json-dns"
"github.com/miekg/dns"
"golang.org/x/net/idna"
)
func (s *Server) parseRequestGoogle(w http.ResponseWriter, r *http.Request) *DNSRequest {
name := r.FormValue("name")
if name == "" {
return &DNSRequest{
errcode: 400,
errtext: "Invalid argument value: \"name\"",
}
}
name = strings.ToLower(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" || cdStr == "true" {
cd = true
} else if cdStr == "0" || 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 = 48
}
} 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 = s.findClientIP(r)
if ednsClientAddress == nil {
ednsClientNetmask = 0
} else if ipv4 := ednsClientAddress.To4(); ipv4 != nil {
ednsClientFamily = 1
ednsClientAddress = ipv4
ednsClientNetmask = 24
} else {
ednsClientFamily = 2
ednsClientNetmask = 48
}
}
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(4096)
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 (s *Server) generateResponseGoogle(w http.ResponseWriter, r *http.Request, req *DNSRequest) {
respJSON := jsonDNS.Marshal(req.response)
respStr, err := json.Marshal(respJSON)
if err != nil {
log.Println(err)
jsonDNS.FormatError(w, fmt.Sprintf("DNS packet parse failure (%s)", err.Error()), 500)
return
}
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
if respJSON.HaveTTL {
if req.isTailored {
w.Header().Set("Cache-Control", "public, max-age="+strconv.Itoa(int(respJSON.LeastTTL)))
} else {
w.Header().Set("Cache-Control", "private, max-age="+strconv.Itoa(int(respJSON.LeastTTL)))
}
w.Header().Set("Expires", respJSON.EarliestExpires.Format(http.TimeFormat))
}
if respJSON.Status == dns.RcodeServerFailure {
w.WriteHeader(503)
}
w.Write(respStr)
}

144
doh-server/ietf.go Normal file
View File

@@ -0,0 +1,144 @@
/*
DNS-over-HTTPS
Copyright (C) 2017-2018 Star Brilliant <m13253@hotmail.com>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
package main
import (
"encoding/base64"
"fmt"
"io/ioutil"
"log"
"net/http"
"strconv"
"time"
"../json-dns"
"github.com/miekg/dns"
)
func (s *Server) parseRequestIETF(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-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\""),
}
}
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()),
}
}
msg.Id = dns.Id()
opt := msg.IsEdns0()
if opt == nil {
opt = new(dns.OPT)
opt.Hdr.Name = "."
opt.Hdr.Rrtype = dns.TypeOPT
opt.SetUDPSize(4096)
opt.SetDo(false)
msg.Extra = append(msg.Extra, opt)
}
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 := s.findClientIP(r)
ednsClientNetmask := uint8(255)
if ednsClientAddress != nil {
if ipv4 := ednsClientAddress.To4(); ipv4 != nil {
ednsClientFamily = 1
ednsClientAddress = ipv4
ednsClientNetmask = 24
} else {
ednsClientFamily = 2
ednsClientNetmask = 48
}
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,
isTailored: isTailored,
}
}
func (s *Server) generateResponseIETF(w http.ResponseWriter, r *http.Request, req *DNSRequest) {
respJSON := jsonDNS.Marshal(req.response)
req.response.Id = 0
respBytes, err := req.response.Pack()
if err != nil {
log.Println(err)
jsonDNS.FormatError(w, fmt.Sprintf("DNS packet construct failure (%s)", err.Error()), 500)
return
}
w.Header().Set("Content-Type", "application/dns-udpwireformat")
now := time.Now().Format(http.TimeFormat)
w.Header().Set("Date", now)
w.Header().Set("Last-Modified", now)
if respJSON.HaveTTL {
if req.isTailored {
w.Header().Set("Cache-Control", "public, max-age="+strconv.Itoa(int(respJSON.LeastTTL)))
} else {
w.Header().Set("Cache-Control", "private, max-age="+strconv.Itoa(int(respJSON.LeastTTL)))
}
w.Header().Set("Expires", respJSON.EarliestExpires.Format(http.TimeFormat))
}
if respJSON.Status == dns.RcodeServerFailure {
w.WriteHeader(503)
}
w.Write(respBytes)
}

View File

@@ -1,6 +1,6 @@
/* /*
DNS-over-HTTPS DNS-over-HTTPS
Copyright (C) 2017 Star Brilliant <m13253@hotmail.com> Copyright (C) 2017-2018 Star Brilliant <m13253@hotmail.com>
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -1,6 +1,6 @@
/* /*
DNS-over-HTTPS DNS-over-HTTPS
Copyright (C) 2017 Star Brilliant <m13253@hotmail.com> Copyright (C) 2017-2018 Star Brilliant <m13253@hotmail.com>
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -24,20 +24,18 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"math/rand"
"log" "log"
"math/rand"
"net" "net"
"net/http" "net/http"
"os" "os"
"strconv"
"strings" "strings"
"time" "time"
"golang.org/x/net/idna"
"../json-dns"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/miekg/dns" "github.com/miekg/dns"
"../json-dns"
) )
type Server struct { type Server struct {
@@ -47,14 +45,22 @@ type Server struct {
servemux *http.ServeMux servemux *http.ServeMux
} }
type DNSRequest struct {
request *dns.Msg
response *dns.Msg
isTailored bool
errcode int
errtext string
}
func NewServer(conf *config) (s *Server) { func NewServer(conf *config) (s *Server) {
s = &Server { s = &Server{
conf: conf, conf: conf,
udpClient: &dns.Client { udpClient: &dns.Client{
Net: "udp", Net: "udp",
Timeout: time.Duration(conf.Timeout) * time.Second, Timeout: time.Duration(conf.Timeout) * time.Second,
}, },
tcpClient: &dns.Client { tcpClient: &dns.Client{
Net: "tcp", Net: "tcp",
Timeout: time.Duration(conf.Timeout) * time.Second, Timeout: time.Duration(conf.Timeout) * time.Second,
}, },
@@ -71,151 +77,76 @@ func (s *Server) Start() error {
} }
if s.conf.Cert != "" || s.conf.Key != "" { if s.conf.Cert != "" || s.conf.Key != "" {
return http.ListenAndServeTLS(s.conf.Listen, s.conf.Cert, s.conf.Key, servemux) return http.ListenAndServeTLS(s.conf.Listen, s.conf.Cert, s.conf.Key, servemux)
} else {
return http.ListenAndServe(s.conf.Listen, servemux)
} }
return http.ListenAndServe(s.conf.Listen, servemux)
} }
func (s *Server) handlerFunc(w http.ResponseWriter, r *http.Request) { func (s *Server) handlerFunc(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.Header().Set("Server", "DNS-over-HTTPS/1.0 (+https://github.com/m13253/dns-over-https)") w.Header().Set("Server", "DNS-over-HTTPS/1.0 (+https://github.com/m13253/dns-over-https)")
w.Header().Set("X-Powered-By", "DNS-over-HTTPS/1.0 (+https://github.com/m13253/dns-over-https)") w.Header().Set("X-Powered-By", "DNS-over-HTTPS/1.0 (+https://github.com/m13253/dns-over-https)")
name := r.FormValue("name") if r.Form == nil {
if name == "" { const maxMemory = 32 << 20 // 32 MB
jsonDNS.FormatError(w, "Invalid argument value: \"name\"", 400) r.ParseMultipartForm(maxMemory)
return
} }
name = strings.ToLower(name) contentType := r.Header.Get("Content-Type")
if punycode, err := idna.ToASCII(name); err == nil { if ct := r.FormValue("ct"); ct != "" {
name = punycode contentType = ct
} else {
jsonDNS.FormatError(w, fmt.Sprintf("Invalid argument value: \"name\" = %q (%s)", name, err.Error()), 400)
return
} }
if contentType == "" {
rrTypeStr := r.FormValue("type") // Guess request Content-Type based on other parameters
rrType := uint16(1) if r.FormValue("name") != "" {
if rrTypeStr == "" { contentType = "application/x-www-form-urlencoded"
} else if v, err := strconv.ParseUint(rrTypeStr, 10, 16); err == nil { } else if r.FormValue("dns") != "" {
rrType = uint16(v) contentType = "application/dns-udpwireformat"
} else if v, ok := dns.StringToType[strings.ToUpper(rrTypeStr)]; ok {
rrType = v
} else {
jsonDNS.FormatError(w, fmt.Sprintf("Invalid argument value: \"type\" = %q", rrTypeStr), 400)
return
} }
cdStr := r.FormValue("cd")
cd := false
if cdStr == "1" || cdStr == "true" {
cd = true
} else if cdStr == "0" || cdStr == "false" || cdStr == "" {
} else {
jsonDNS.FormatError(w, fmt.Sprintf("Invalid argument value: \"cd\" = %q", cdStr), 400)
return
} }
var responseType string
ednsClientSubnet := r.FormValue("edns_client_subnet") for _, responseCandidate := range strings.Split(r.Header.Get("Accept"), ",") {
ednsClientFamily := uint16(0) responseCandidate = strings.ToLower(strings.SplitN(responseCandidate, ";", 2)[0])
ednsClientAddress := net.IP(nil) if responseCandidate == "application/json" {
ednsClientNetmask := uint8(255) responseType = "application/json"
if ednsClientSubnet != "" { break
if ednsClientSubnet == "0/0" { } else if responseCandidate == "application/dns-udpwireformat" {
ednsClientSubnet = "0.0.0.0/0" responseType = "application/dns-udpwireformat"
break
} }
slash := strings.IndexByte(ednsClientSubnet, '/')
if slash < 0 {
ednsClientAddress = net.ParseIP(ednsClientSubnet)
if ednsClientAddress == nil {
jsonDNS.FormatError(w, fmt.Sprintf("Invalid argument value: \"edns_client_subnet\" = %q", ednsClientSubnet), 400)
return
} }
if ipv4 := ednsClientAddress.To4(); ipv4 != nil { if responseType == "" {
ednsClientFamily = 1 // Guess response Content-Type based on request Content-Type
ednsClientAddress = ipv4 if contentType == "application/x-www-form-urlencoded" {
ednsClientNetmask = 24 responseType = "application/json"
} else { } else if contentType == "application/dns-udpwireformat" {
ednsClientFamily = 2 responseType = "application/dns-udpwireformat"
ednsClientNetmask = 48
}
} else {
ednsClientAddress = net.ParseIP(ednsClientSubnet[:slash])
if ednsClientAddress == nil {
jsonDNS.FormatError(w, fmt.Sprintf("Invalid argument value: \"edns_client_subnet\" = %q", ednsClientSubnet), 400)
return
}
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 {
jsonDNS.FormatError(w, fmt.Sprintf("Invalid argument value: \"edns_client_subnet\" = %q (%s)", ednsClientSubnet, err.Error()), 400)
return
}
ednsClientNetmask = uint8(netmask)
}
} else {
ednsClientAddress = s.findClientIP(r)
if ednsClientAddress == nil {
ednsClientNetmask = 0
} else if ipv4 := ednsClientAddress.To4(); ipv4 != nil {
ednsClientFamily = 1
ednsClientAddress = ipv4
ednsClientNetmask = 24
} else {
ednsClientFamily = 2
ednsClientNetmask = 48
} }
} }
msg := new(dns.Msg) var req *DNSRequest
msg.SetQuestion(dns.Fqdn(name), rrType) if contentType == "application/x-www-form-urlencoded" {
msg.CheckingDisabled = cd req = s.parseRequestGoogle(w, r)
opt := new(dns.OPT) } else if contentType == "application/dns-udpwireformat" {
opt.Hdr.Name = "." req = s.parseRequestIETF(w, r)
opt.Hdr.Rrtype = dns.TypeOPT } else {
opt.SetUDPSize(4096) jsonDNS.FormatError(w, fmt.Sprintf("Invalid argument value: \"ct\" = %q", contentType), 400)
opt.SetDo(true) return
if ednsClientAddress != nil { }
edns0Subnet := new(dns.EDNS0_SUBNET) if req.errcode != 0 {
edns0Subnet.Code = dns.EDNS0SUBNET jsonDNS.FormatError(w, req.errtext, req.errcode)
edns0Subnet.Family = ednsClientFamily return
edns0Subnet.SourceNetmask = ednsClientNetmask
edns0Subnet.SourceScope = 0
edns0Subnet.Address = ednsClientAddress
opt.Option = append(opt.Option, edns0Subnet)
} }
msg.Extra = append(msg.Extra, opt)
resp, err := s.doDNSQuery(msg) var err error
req.response, err = s.doDNSQuery(req.request)
if err != nil { if err != nil {
jsonDNS.FormatError(w, fmt.Sprintf("DNS query failure (%s)", err.Error()), 503) jsonDNS.FormatError(w, fmt.Sprintf("DNS query failure (%s)", err.Error()), 503)
return return
} }
respJson := jsonDNS.Marshal(resp)
respStr, err := json.Marshal(respJson)
if err != nil {
log.Println(err)
jsonDNS.FormatError(w, fmt.Sprintf("DNS packet parse failure (%s)", err.Error()), 500)
return
}
if respJson.HaveTTL { if contentType == "application/x-www-form-urlencoded" {
if ednsClientSubnet != "" { s.generateResponseGoogle(w, r, req)
w.Header().Set("Cache-Control", "public, max-age=" + strconv.Itoa(int(respJson.LeastTTL))) } else if contentType == "application/dns-udpwireformat" {
} else { s.generateResponseIETF(w, r, req)
w.Header().Set("Cache-Control", "private, max-age=" + strconv.Itoa(int(respJson.LeastTTL)))
} }
w.Header().Set("Expires", respJson.EarliestExpires.Format(http.TimeFormat))
}
if respJson.Status == dns.RcodeServerFailure {
w.WriteHeader(503)
}
w.Write(respStr)
} }
func (s *Server) findClientIP(r *http.Request) net.IP { func (s *Server) findClientIP(r *http.Request) net.IP {
@@ -248,9 +179,9 @@ func (s *Server) findClientIP(r *http.Request) net.IP {
} }
func (s *Server) doDNSQuery(msg *dns.Msg) (resp *dns.Msg, err error) { func (s *Server) doDNSQuery(msg *dns.Msg) (resp *dns.Msg, err error) {
num_servers := len(s.conf.Upstream) numServers := len(s.conf.Upstream)
for i := uint(0); i < s.conf.Tries; i++ { for i := uint(0); i < s.conf.Tries; i++ {
server := s.conf.Upstream[rand.Intn(num_servers)] server := s.conf.Upstream[rand.Intn(numServers)]
if !s.conf.TCPOnly { if !s.conf.TCPOnly {
resp, _, err = s.udpClient.Exchange(msg, server) resp, _, err = s.udpClient.Exchange(msg, server)
if err == dns.ErrTruncated { if err == dns.ErrTruncated {

View File

@@ -1,6 +1,6 @@
/* /*
DNS-over-HTTPS DNS-over-HTTPS
Copyright (C) 2017 Star Brilliant <m13253@hotmail.com> Copyright (C) 2017-2018 Star Brilliant <m13253@hotmail.com>
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -27,6 +27,7 @@ import (
"encoding/json" "encoding/json"
"log" "log"
"net/http" "net/http"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@@ -36,7 +37,8 @@ type dnsError struct {
} }
func FormatError(w http.ResponseWriter, comment string, errcode int) { func FormatError(w http.ResponseWriter, comment string, errcode int) {
errJson := dnsError { w.Header().Set("Content-Type", "application/json; charset=UTF-8")
errJson := dnsError{
Status: dns.RcodeServerFailure, Status: dns.RcodeServerFailure,
Comment: comment, Comment: comment,
} }

View File

@@ -1,6 +1,6 @@
/* /*
DNS-over-HTTPS DNS-over-HTTPS
Copyright (C) 2017 Star Brilliant <m13253@hotmail.com> Copyright (C) 2017-2018 Star Brilliant <m13253@hotmail.com>
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -28,80 +28,80 @@ import (
) )
// RFC6890 // RFC6890
var localIPv4Nets = []net.IPNet { var localIPv4Nets = []net.IPNet{
// This host on this network // This host on this network
net.IPNet { net.IPNet{
net.IP { 0, 0, 0, 0 }, net.IP{0, 0, 0, 0},
net.IPMask { 255, 0, 0, 0 }, net.IPMask{255, 0, 0, 0},
}, },
// Private-Use Networks // Private-Use Networks
net.IPNet { net.IPNet{
net.IP { 10, 0, 0, 0 }, net.IP{10, 0, 0, 0},
net.IPMask { 255, 0, 0, 0 }, net.IPMask{255, 0, 0, 0},
}, },
// Shared Address Space // Shared Address Space
net.IPNet { net.IPNet{
net.IP { 100, 64, 0, 0 }, net.IP{100, 64, 0, 0},
net.IPMask { 255, 192, 0, 0 }, net.IPMask{255, 192, 0, 0},
}, },
// Loopback // Loopback
net.IPNet { net.IPNet{
net.IP { 127, 0, 0, 0 }, net.IP{127, 0, 0, 0},
net.IPMask { 255, 0, 0, 0 }, net.IPMask{255, 0, 0, 0},
}, },
// Link Local // Link Local
net.IPNet { net.IPNet{
net.IP { 169, 254, 0, 0 }, net.IP{169, 254, 0, 0},
net.IPMask { 255, 255, 0, 0 }, net.IPMask{255, 255, 0, 0},
}, },
// Private-Use Networks // Private-Use Networks
net.IPNet { net.IPNet{
net.IP { 172, 16, 0, 0 }, net.IP{172, 16, 0, 0},
net.IPMask { 255, 240, 0, 0 }, net.IPMask{255, 240, 0, 0},
}, },
// DS-Lite // DS-Lite
net.IPNet { net.IPNet{
net.IP { 192, 0, 0, 0 }, net.IP{192, 0, 0, 0},
net.IPMask { 255, 255, 255, 248 }, net.IPMask{255, 255, 255, 248},
}, },
// 6to4 Relay Anycast // 6to4 Relay Anycast
net.IPNet { net.IPNet{
net.IP { 192, 88, 99, 0 }, net.IP{192, 88, 99, 0},
net.IPMask { 255, 255, 255, 0 }, net.IPMask{255, 255, 255, 0},
}, },
// Private-Use Networks // Private-Use Networks
net.IPNet { net.IPNet{
net.IP { 192, 168, 0, 0 }, net.IP{192, 168, 0, 0},
net.IPMask { 255, 255, 0, 0 }, net.IPMask{255, 255, 0, 0},
}, },
// Reserved for Future Use & Limited Broadcast // Reserved for Future Use & Limited Broadcast
net.IPNet { net.IPNet{
net.IP { 240, 0, 0, 0 }, net.IP{240, 0, 0, 0},
net.IPMask { 240, 0, 0, 0 }, net.IPMask{240, 0, 0, 0},
}, },
} }
// RFC6890 // RFC6890
var localIPv6Nets = []net.IPNet { var localIPv6Nets = []net.IPNet{
// Unspecified & Loopback Address // Unspecified & Loopback Address
net.IPNet { net.IPNet{
net.IP { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, 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 }, net.IPMask{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe},
}, },
// Discard-Only Prefix // Discard-Only Prefix
net.IPNet { net.IPNet{
net.IP { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, 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 }, net.IPMask{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
}, },
// Unique-Local // Unique-Local
net.IPNet { net.IPNet{
net.IP { 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, 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 }, net.IPMask{0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
}, },
// Linked-Scoped Unicast // Linked-Scoped Unicast
net.IPNet { net.IPNet{
net.IP { 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, 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 }, net.IPMask{0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
}, },
} }

View File

@@ -1,6 +1,6 @@
/* /*
DNS-over-HTTPS DNS-over-HTTPS
Copyright (C) 2017 Star Brilliant <m13253@hotmail.com> Copyright (C) 2017-2018 Star Brilliant <m13253@hotmail.com>
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -28,6 +28,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@@ -44,7 +45,7 @@ func Marshal(msg *dns.Msg) *Response {
resp.Question = make([]Question, 0, len(msg.Question)) resp.Question = make([]Question, 0, len(msg.Question))
for _, question := range msg.Question { for _, question := range msg.Question {
jsonQuestion := Question { jsonQuestion := Question{
Name: question.Name, Name: question.Name,
Type: question.Qtype, Type: question.Qtype,
} }
@@ -85,7 +86,7 @@ func Marshal(msg *dns.Msg) *Response {
edns0 := option.(*dns.EDNS0_SUBNET) edns0 := option.(*dns.EDNS0_SUBNET)
clientAddress := edns0.Address clientAddress := edns0.Address
if clientAddress == nil { if clientAddress == nil {
clientAddress = net.IP { 0, 0, 0, 0 } clientAddress = net.IP{0, 0, 0, 0}
} else if ipv4 := clientAddress.To4(); ipv4 != nil { } else if ipv4 := clientAddress.To4(); ipv4 != nil {
clientAddress = ipv4 clientAddress = ipv4
} }
@@ -106,7 +107,7 @@ func Marshal(msg *dns.Msg) *Response {
} }
func marshalRR(rr dns.RR, now time.Time) RR { func marshalRR(rr dns.RR, now time.Time) RR {
jsonRR := RR {} jsonRR := RR{}
rrHeader := rr.Header() rrHeader := rr.Header()
jsonRR.Name = rrHeader.Name jsonRR.Name = rrHeader.Name
jsonRR.Type = rrHeader.Rrtype jsonRR.Type = rrHeader.Rrtype

View File

@@ -1,6 +1,6 @@
/* /*
DNS-over-HTTPS DNS-over-HTTPS
Copyright (C) 2017 Star Brilliant <m13253@hotmail.com> Copyright (C) 2017-2018 Star Brilliant <m13253@hotmail.com>
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),

View File

@@ -1,6 +1,6 @@
/* /*
DNS-over-HTTPS DNS-over-HTTPS
Copyright (C) 2017 Star Brilliant <m13253@hotmail.com> Copyright (C) 2017-2018 Star Brilliant <m13253@hotmail.com>
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"), copy of this software and associated documentation files (the "Software"),
@@ -30,6 +30,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@@ -77,7 +78,7 @@ func Unmarshal(msg *dns.Msg, resp *Response, udpSize uint16, ednsClientNetmask u
} }
} }
reply.Extra = make([]dns.RR, 0, len(resp.Additional) + 1) reply.Extra = make([]dns.RR, 0, len(resp.Additional)+1)
opt := new(dns.OPT) opt := new(dns.OPT)
opt.Hdr.Name = "." opt.Hdr.Name = "."
opt.Hdr.Rrtype = dns.TypeOPT opt.Hdr.Rrtype = dns.TypeOPT
@@ -94,20 +95,20 @@ func Unmarshal(msg *dns.Msg, resp *Response, udpSize uint16, ednsClientNetmask u
if ednsClientSubnet != "" { if ednsClientSubnet != "" {
slash := strings.IndexByte(ednsClientSubnet, '/') slash := strings.IndexByte(ednsClientSubnet, '/')
if slash < 0 { if slash < 0 {
log.Println(UnmarshalError { "Invalid client subnet" }) log.Println(UnmarshalError{"Invalid client subnet"})
} else { } else {
ednsClientAddress = net.ParseIP(ednsClientSubnet[:slash]) ednsClientAddress = net.ParseIP(ednsClientSubnet[:slash])
if ednsClientAddress == nil { if ednsClientAddress == nil {
log.Println(UnmarshalError { "Invalid client subnet address" }) log.Println(UnmarshalError{"Invalid client subnet address"})
} else if ipv4 := ednsClientAddress.To4(); ipv4 != nil { } else if ipv4 := ednsClientAddress.To4(); ipv4 != nil {
ednsClientFamily = 1 ednsClientFamily = 1
ednsClientAddress = ipv4 ednsClientAddress = ipv4
} else { } else {
ednsClientFamily = 2 ednsClientFamily = 2
} }
scope, err := strconv.ParseUint(ednsClientSubnet[slash + 1:], 10, 8) scope, err := strconv.ParseUint(ednsClientSubnet[slash+1:], 10, 8)
if err != nil { if err != nil {
log.Println(UnmarshalError { "Invalid client subnet address" }) log.Println(UnmarshalError{"Invalid client subnet address"})
} else { } else {
ednsClientScope = uint8(scope) ednsClientScope = uint8(scope)
} }
@@ -147,12 +148,12 @@ func Unmarshal(msg *dns.Msg, resp *Response, udpSize uint16, ednsClientNetmask u
func unmarshalRR(rr RR, now time.Time) (dnsRR dns.RR, err error) { func unmarshalRR(rr RR, now time.Time) (dnsRR dns.RR, err error) {
if strings.ContainsAny(rr.Name, "\t\r\n \"();\\") { if strings.ContainsAny(rr.Name, "\t\r\n \"();\\") {
return nil, UnmarshalError { fmt.Sprintf("Record name contains space: %q", rr.Name) } return nil, UnmarshalError{fmt.Sprintf("Record name contains space: %q", rr.Name)}
} }
if rr.ExpiresStr != "" { if rr.ExpiresStr != "" {
rr.Expires, err = time.Parse(time.RFC1123, rr.ExpiresStr) rr.Expires, err = time.Parse(time.RFC1123, rr.ExpiresStr)
if err != nil { if err != nil {
return nil, UnmarshalError { fmt.Sprintf("Invalid expire time: %q", rr.ExpiresStr) } return nil, UnmarshalError{fmt.Sprintf("Invalid expire time: %q", rr.ExpiresStr)}
} }
ttl := rr.Expires.Sub(now) / time.Second ttl := rr.Expires.Sub(now) / time.Second
if ttl >= 0 && ttl <= 0xffffffff { if ttl >= 0 && ttl <= 0xffffffff {
@@ -161,10 +162,10 @@ func unmarshalRR(rr RR, now time.Time) (dnsRR dns.RR, err error) {
} }
rrType, ok := dns.TypeToString[rr.Type] rrType, ok := dns.TypeToString[rr.Type]
if !ok { if !ok {
return nil, UnmarshalError { fmt.Sprintf("Unknown record type: %d", rr.Type) } return nil, UnmarshalError{fmt.Sprintf("Unknown record type: %d", rr.Type)}
} }
if strings.ContainsAny(rr.Data, "\r\n") { if strings.ContainsAny(rr.Data, "\r\n") {
return nil, UnmarshalError { fmt.Sprintf("Record data contains newline: %q", rr.Data) } 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) zone := fmt.Sprintf("%s %d IN %s %s", rr.Name, rr.TTL, rrType, rr.Data)
dnsRR, err = dns.NewRR(zone) dnsRR, err = dns.NewRR(zone)