Browse Source

Initial import

master
maze 1 year ago
parent
commit
1db7d58651
10 changed files with 1217 additions and 0 deletions
  1. +36
    -0
      cmd/doh/main.go
  2. +104
    -0
      globalip.go
  3. +11
    -0
      go.mod
  4. +63
    -0
      go.sum
  5. +277
    -0
      json.go
  6. +313
    -0
      server.go
  7. +174
    -0
      server_google.go
  8. +195
    -0
      server_ietf.go
  9. +17
    -0
      testdata/server.crt
  10. +27
    -0
      testdata/server.key

+ 36
- 0
cmd/doh/main.go View File

@ -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)
}
}

+ 104
- 0
globalip.go View File

@ -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
}

+ 11
- 0
go.mod View File

@ -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
)

+ 63
- 0
go.sum View File

@ -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=

+ 277
- 0
json.go View File

@ -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
}

+ 313
- 0
server.go View File

@ -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
}

+ 174
- 0
server_google.go View File

@ -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)
}

+ 195
- 0
server_ietf.go View File

@ -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
}

+ 17
- 0
testdata/server.crt View File

@ -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-----

+ 27
- 0
testdata/server.key View File

@ -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-----

Loading…
Cancel
Save