This commit is contained in:
Star Brilliant
2017-09-18 00:55:31 +08:00
parent 17c9ef0665
commit 1f8b1ea7ad
10 changed files with 1092 additions and 0 deletions

24
Makefile Normal file
View File

@@ -0,0 +1,24 @@
.PHONY: all clean install uninstall
GOBUILD=go build
PREFIX=/usr/local
all: doh-client/doh-client doh-server/doh-server
clean:
rm -f doh-client/doh-client doh-server/doh-server
install: doh-client/doh-client doh-server/doh-server
install -Dm0755 doh-client/doh-client $(PREFIX)/bin/doh-client
install -Dm0755 doh-server/doh-server $(PREFIX)/bin/doh-server
setcap cap_net_bind_service=+ep $(PREFIX)/bin/doh-client
setcap cap_net_bind_service=+ep $(PREFIX)/bin/doh-server
uninstall:
rm -f $(PREFIX)/bin/doh-client $(PREFIX)/bin/doh-server
doh-client/doh-client: doh-client/client.go doh-client/main.go json-dns/error.go json-dns/globalip.go json-dns/marshal.go json-dns/response.go json-dns/unmarshal.go
cd doh-client && $(GOBUILD)
doh-server/doh-server: doh-server/main.go doh-server/server.go json-dns/error.go json-dns/globalip.go json-dns/marshal.go json-dns/response.go json-dns/unmarshal.go
cd doh-server && $(GOBUILD)

209
doh-client/client.go Normal file
View File

@@ -0,0 +1,209 @@
/*
DNS-over-HTTPS
Copyright (C) 2017 Star Brilliant <m13253@hotmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"strconv"
"time"
"github.com/miekg/dns"
"../json-dns"
)
type Client struct {
addr string
upstream string
udpServer *dns.Server
tcpServer *dns.Server
}
func NewClient(addr, upstream string) (c *Client) {
c = &Client {
addr: addr,
upstream: upstream,
}
c.udpServer = &dns.Server {
Addr: addr,
Net: "udp",
Handler: dns.HandlerFunc(c.udpHandlerFunc),
UDPSize: 4096,
}
c.tcpServer = &dns.Server {
Addr: addr,
Net: "tcp",
Handler: dns.HandlerFunc(c.tcpHandlerFunc),
}
return
}
func (c *Client) Start() error {
result := make(chan error)
go func() {
err := c.udpServer.ListenAndServe()
if err != nil {
log.Println(err)
}
result <- err
} ()
go func() {
err := c.tcpServer.ListenAndServe()
if err != nil {
log.Println(err)
}
result <- err
} ()
err := <-result
if err != nil {
return err
}
err = <-result
return err
}
func (c *Client) handlerFunc(w dns.ResponseWriter, r *dns.Msg, isTCP bool) {
if r.Response == true {
log.Println("Received a response packet")
return
}
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]
questionName := question.Name
questionType := ""
if qtype, ok := dns.TypeToString[question.Qtype]; ok {
questionType = qtype
} else {
questionType = strconv.Itoa(int(question.Qtype))
}
fmt.Printf("%s - - [%s] \"%s IN %s\"\n", w.RemoteAddr(), time.Now().Format("02/Jan/2006:15:04:05 -0700"), questionName, questionType)
requestURL := fmt.Sprintf("%s?name=%s&type=%s", c.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)
}
resp, err := http.Get(requestURL)
if err != nil {
log.Println(err)
reply.Rcode = dns.RcodeServerFailure
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
}
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) {
c.handlerFunc(w, r, false)
}
func (c *Client) tcpHandlerFunc(w dns.ResponseWriter, r *dns.Msg) {
c.handlerFunc(w, r, true)
}
var (
ipv4Mask24 net.IPMask = net.IPMask { 255, 255, 255, 0 }
ipv6Mask48 net.IPMask = net.CIDRMask(48, 128)
)
func (c *Client) findClientIP(w dns.ResponseWriter, r *dns.Msg) (ednsClientAddress net.IP, ednsClientNetmask uint8) {
ednsClientNetmask = 255
opt := r.IsEdns0()
for _, option := range opt.Option {
if option.Option() == dns.EDNS0SUBNET {
edns0Subnet := option.(*dns.EDNS0_SUBNET)
ednsClientAddress = edns0Subnet.Address
ednsClientNetmask = edns0Subnet.SourceNetmask
return
}
}
remoteAddr, err := net.ResolveUDPAddr("udp", w.RemoteAddr().String())
if err != nil {
return
}
if ip := remoteAddr.IP; jsonDNS.IsGlobalIP(ip) {
if ipv4 := ip.To4(); ipv4 != nil {
ednsClientAddress = ipv4.Mask(ipv4Mask24)
ednsClientNetmask = 24
} else {
ednsClientAddress = ip.Mask(ipv6Mask48)
ednsClientNetmask = 48
}
}
return
}

32
doh-client/main.go Normal file
View File

@@ -0,0 +1,32 @@
/*
DNS-over-HTTPS
Copyright (C) 2017 Star Brilliant <m13253@hotmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"flag"
)
func main() {
addr := flag.String("addr", ":5533", "DNS listen port")
upstream := flag.String("upstream", "http://localhost:8080/resolve", "HTTP path for upstream resolver")
flag.Parse()
client := NewClient(*addr, *upstream)
_ = client.Start()
}

46
doh-server/main.go Normal file
View File

@@ -0,0 +1,46 @@
/*
DNS-over-HTTPS
Copyright (C) 2017 Star Brilliant <m13253@hotmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"flag"
"log"
"strings"
)
func main() {
addr := flag.String("addr", ":8080", "HTTP listen port")
cert := flag.String("cert", "", "TLS certification file")
key := flag.String("key", "", "TLS key file")
path := flag.String("path", "/resolve", "HTTP path for resolve application")
upstream := flag.String("upstream", "8.8.8.8:53,8.8.4.4:53", "Upstream DNS resolver")
tcpOnly := flag.Bool("tcp", false, "Only use TCP for DNS query")
flag.Parse()
if (*cert != "") != (*key != "") {
log.Fatalln("You must specify both -cert and -key to enable TLS")
}
upstreams := strings.Split(*upstream, ",")
server := NewServer(*addr, *cert, *key, *path, upstreams, *tcpOnly)
err := server.Start()
if err != nil {
log.Fatalln(err)
}
}

270
doh-server/server.go Normal file
View File

@@ -0,0 +1,270 @@
/*
DNS-over-HTTPS
Copyright (C) 2017 Star Brilliant <m13253@hotmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"encoding/json"
"fmt"
"math/rand"
"log"
"net"
"net/http"
"os"
"strconv"
"strings"
"time"
"golang.org/x/net/idna"
"github.com/gorilla/handlers"
"github.com/miekg/dns"
"../json-dns"
)
type Server struct {
addr string
cert string
key string
path string
upstreams []string
tcpOnly bool
udpClient *dns.Client
tcpClient *dns.Client
servemux *http.ServeMux
}
func NewServer(addr, cert, key, path string, upstreams []string, tcpOnly bool) (s *Server) {
upstreamsCopy := make([]string, len(upstreams))
copy(upstreamsCopy, upstreams)
s = &Server {
addr: addr,
cert: cert,
key: key,
path: path,
upstreams: upstreamsCopy,
tcpOnly: tcpOnly,
udpClient: &dns.Client {
Net: "udp",
},
tcpClient: &dns.Client {
Net: "tcp",
},
servemux: http.NewServeMux(),
}
s.servemux.HandleFunc(path, s.handlerFunc)
return
}
func (s *Server) Start() error {
if s.cert != "" || s.key != "" {
return http.ListenAndServeTLS(s.addr, s.cert, s.key, handlers.CombinedLoggingHandler(os.Stdout, s.servemux))
} else {
return http.ListenAndServe(s.addr, handlers.CombinedLoggingHandler(os.Stdout, s.servemux))
}
}
func (s *Server) handlerFunc(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
name := r.FormValue("name")
if name == "" {
http.Error(w, jsonDNS.FormatError("Invalid argument value: \"name\""), 400)
return
}
if punycode, err := idna.ToASCII(name); err == nil {
name = punycode
} else {
http.Error(w, jsonDNS.FormatError(fmt.Sprintf("Invalid argument value: \"name\" = %q (%s)", name, err.Error())), 400)
return
}
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 {
http.Error(w, jsonDNS.FormatError(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 {
http.Error(w, jsonDNS.FormatError(fmt.Sprintf("Invalid argument value: \"cd\" = %q", cdStr)), 400)
return
}
ednsClientSubnet := r.FormValue("edns_client_subnet")
ednsClientFamily := uint16(0)
ednsClientAddress := net.IP(nil)
ednsClientNetmask := uint8(255)
if ednsClientSubnet != "" {
slash := strings.IndexByte(ednsClientSubnet, '/')
if slash < 0 {
ednsClientAddress = net.ParseIP(ednsClientSubnet)
if ednsClientAddress == nil {
http.Error(w, jsonDNS.FormatError(fmt.Sprintf("Invalid argument value: \"edns_client_subnet\" = %q", ednsClientSubnet)), 400)
return
}
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 {
http.Error(w, jsonDNS.FormatError(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 {
http.Error(w, jsonDNS.FormatError(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)
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)
resp, err := s.doDNSQuery(msg)
if err != nil {
http.Error(w, jsonDNS.FormatError(fmt.Sprintf("DNS query failure (%s)", err.Error())), 503)
return
}
respJson := jsonDNS.Marshal(resp)
respStr, err := json.Marshal(respJson)
if err != nil {
http.Error(w, jsonDNS.FormatError(fmt.Sprintf("DNS packet parse failure (%s)", err.Error())), 500)
return
}
if respJson.HaveTTL {
w.Header().Set("Cache-Control", "max-age=" + strconv.Itoa(int(respJson.LeastTTL)))
w.Header().Set("Expires", respJson.EarliestExpires.Format(time.RFC1123))
}
w.Write(respStr)
}
func (s *Server) findClientIP(r *http.Request) net.IP {
XForwardedFor := r.Header.Get("X-Forwarded-For")
if XForwardedFor != "" {
for _, addr := range strings.Split(XForwardedFor, ",") {
addr = strings.TrimSpace(addr)
ip := net.ParseIP(addr)
if jsonDNS.IsGlobalIP(ip) {
return ip
}
}
}
XRealIP := r.Header.Get("X-Real-IP")
if XRealIP != "" {
addr := strings.TrimSpace(XRealIP)
ip := net.ParseIP(addr)
if jsonDNS.IsGlobalIP(ip) {
return ip
}
}
remoteAddr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr)
if err != nil {
return nil
}
if ip := remoteAddr.IP; jsonDNS.IsGlobalIP(ip) {
return ip
}
return nil
}
func (s *Server) doDNSQuery(msg *dns.Msg) (resp *dns.Msg, err error) {
num_servers := len(s.upstreams)
init_server := rand.Intn(num_servers)
for i := 0; i < num_servers; i++ {
var server string
if init_server + i < num_servers {
server = s.upstreams[i + init_server]
} else {
server = s.upstreams[i + init_server - num_servers]
}
if !s.tcpOnly {
resp, _, err = s.udpClient.Exchange(msg, server)
if err == dns.ErrTruncated {
log.Println(err)
resp, _, err = s.tcpClient.Exchange(msg, server)
if err == dns.ErrTruncated {
log.Println(err)
return
}
}
} else {
resp, _, err = s.tcpClient.Exchange(msg, server)
if err == dns.ErrTruncated {
log.Println(err)
return
}
}
if err == nil {
return
}
log.Println(err)
}
return
}

42
json-dns/error.go Normal file
View File

@@ -0,0 +1,42 @@
/*
DNS-over-HTTPS
Copyright (C) 2017 Star Brilliant <m13253@hotmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package jsonDNS
import (
"encoding/json"
"log"
"github.com/miekg/dns"
)
type dnsError struct {
Status uint32 `json:"Status"`
Comment string `json:"Comment,omitempty"`
}
func FormatError(comment string) string {
errJson := dnsError {
Status: dns.RcodeServerFailure,
Comment: comment,
}
errStr, err := json.Marshal(errJson)
if err != nil {
log.Fatalln(err)
}
return string(errStr)
}

124
json-dns/globalip.go Normal file
View File

@@ -0,0 +1,124 @@
/*
DNS-over-HTTPS
Copyright (C) 2017 Star Brilliant <m13253@hotmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package jsonDNS
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 _, ipnet := range localIPv4Nets {
if ipnet.Contains(ip) {
return false
}
}
return true
}
if len(ip) == net.IPv6len {
for _, ipnet := range localIPv6Nets {
if ipnet.Contains(ip) {
return false
}
}
return true
}
return true
}

109
json-dns/marshal.go Normal file
View File

@@ -0,0 +1,109 @@
/*
DNS-over-HTTPS
Copyright (C) 2017 Star Brilliant <m13253@hotmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package jsonDNS
import (
"strconv"
"strings"
"time"
"github.com/miekg/dns"
)
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)
resp.EdnsClientSubnet = edns0.Address.String() + "/" + strconv.Itoa(int(edns0.SourceScope))
}
}
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) RR {
jsonRR := RR {}
rrHeader := rr.Header()
jsonRR.Name = rrHeader.Name
jsonRR.Type = rrHeader.Rrtype
jsonRR.TTL = rrHeader.Ttl
jsonRR.Expires = now.Add(time.Duration(jsonRR.TTL) * time.Second)
jsonRR.ExpiresStr = jsonRR.Expires.Format(time.RFC1123)
data := strings.SplitN(rr.String(), "\t", 5)
if len(data) >= 5 {
jsonRR.Data = data[4]
}
return jsonRR
}

67
json-dns/response.go Normal file
View File

@@ -0,0 +1,67 @@
/*
DNS-over-HTTPS
Copyright (C) 2017 Star Brilliant <m13253@hotmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package jsonDNS
import (
"time"
)
type Response struct {
// Standard DNS response code (32 bit integer)
Status uint32 `json:"Status"`
// Whether the response is truncated
TC bool `json:"TC"`
// Recursion desired
RD bool `json:"RD"`
// Recursion available
RA bool `json:"RA"`
// Whether all response data was validated with DNSSEC
// FIXME: We don't have DNSSEC yet! This bit is not reliable!
AD bool `json:"AD"`
// Whether the client asked to disable DNSSEC
CD bool `json:"CD"`
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"`
// Least time-to-live
HaveTTL bool `json:"-"`
LeastTTL uint32 `json:"-"`
EarliestExpires time.Time `json:"-"`
}
type Question struct {
// FQDN with trailing dot
Name string `json:"name"`
// Standard DNS RR type
Type uint16 `json:"type"`
}
type RR struct {
Question
// Record's time-to-live in seconds
TTL uint32 `json:"TTL"`
// TTL in absolute time
Expires time.Time `json:"-"`
ExpiresStr string `json:"Expires"`
// Data
Data string `json:"data"`
}

169
json-dns/unmarshal.go Normal file
View File

@@ -0,0 +1,169 @@
/*
DNS-over-HTTPS
Copyright (C) 2017 Star Brilliant <m13253@hotmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package jsonDNS
import (
"fmt"
"log"
"net"
"strconv"
"strings"
"time"
"github.com/miekg/dns"
)
func PrepareReply(req *dns.Msg) *dns.Msg {
reply := new(dns.Msg)
reply.Id = req.Id
reply.Response = true
reply.Opcode = reply.Opcode
reply.RecursionDesired = 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
}
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.RecursionDesired = resp.RD
reply.RecursionAvailable = resp.RA
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 {
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
}