commit 18e3b47c0740a3dd5da812384cdde715dbd52ccf Author: afonso Date: Wed Feb 26 17:55:14 2025 +0000 Do53 and DoH (POST) basic queries implemented diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30f3f28 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +**/tls-key-log.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..31d7daf --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# DNS Resolver + +A DNS resolver supporting multiple protocols including DoH, DoT, DoQ, DNSSEC, ODoH, and DNSCrypt. diff --git a/cmd/resolver/main.go b/cmd/resolver/main.go new file mode 100644 index 0000000..2731df6 --- /dev/null +++ b/cmd/resolver/main.go @@ -0,0 +1,78 @@ +package main + +import ( + "log" + + "github.com/afonsofrancof/sdns-perf/internal/protocols/do53" + "github.com/afonsofrancof/sdns-perf/internal/protocols/doh" + "github.com/alecthomas/kong" +) + +type CommonFlags struct { + DomainName string `help:"Domain name to resolve" arg:"" required:""` + QueryType string `help:"Query type" enum:"A,AAAA,MX,TXT,NS,CNAME,SOA,PTR" default:"A"` + Server string `help:"DNS server to use"` + DNSSEC bool `help:"Enable DNSSEC validation"` +} + +type DoHCmd struct { + CommonFlags `embed:""` + HTTP3 bool `help:"Use HTTP/3" name:"http3"` + Path string `help:"The HTTP path for the POST request" name:"path" required:""` + Proxy string `help:"The Proxy to use with ODoH"` +} + +type DoTCmd struct { + CommonFlags +} + +type DoQCmd struct { + CommonFlags +} + +type Do53Cmd struct { + CommonFlags +} + +var cli struct { + Verbose bool `help:"Enable verbose logging" short:"v"` + + DoH DoHCmd `cmd:"doh" help:"Query using DNS-over-HTTPS" name:"doh"` + DoT DoTCmd `cmd:"dot" help:"Query using DNS-over-TLS" name:"dot"` + DoQ DoQCmd `cmd:"doq" help:"Query using DNS-over-QUIC" name:"doq"` + Do53 Do53Cmd `cmd:"doq" help:"Query using plain DNS over UDP" name:"do53"` +} + +func (c *Do53Cmd) Run() error { + return do53.Run(c.DomainName, c.QueryType, c.Server, c.DNSSEC) +} + +func (c *DoHCmd) Run() error { + return doh.Run(c.DomainName, c.QueryType, c.Server, c.Path,c.Proxy, c.DNSSEC) +} + +func (c *DoTCmd) Run() error { + // TODO: Implement DoT query + return nil +} + +func (c *DoQCmd) Run() error { + // TODO: Implement DoQ query + return nil +} + +func main() { + ctx := kong.Parse(&cli, + kong.Name("dns-go"), + kong.Description("A DNS resolver supporting DoH, DoT, and DoQ protocols"), + kong.UsageOnError(), + kong.ConfigureHelp(kong.HelpOptions{ + Compact: true, + Summary: true, + })) + + err := ctx.Run() + if err != nil { + log.Fatalf("Error: %v", err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7b1813b --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/afonsofrancof/sdns-perf + +go 1.24.0 + +require ( + github.com/alecthomas/kong v1.8.1 + golang.org/x/net v0.35.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f14bc2b --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/kong v1.8.1 h1:6aamvWBE/REnR/BCq10EcozmcpUPc5aGI1lPAWdB0EE= +github.com/alecthomas/kong v1.8.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= diff --git a/internal/protocols/dnscrypt/dnscrypt.go b/internal/protocols/dnscrypt/dnscrypt.go new file mode 100644 index 0000000..9939b27 --- /dev/null +++ b/internal/protocols/dnscrypt/dnscrypt.go @@ -0,0 +1,3 @@ +package dnscrypt + +// DNSCrypt resolver implementation diff --git a/internal/protocols/dnssec/dnssec.go b/internal/protocols/dnssec/dnssec.go new file mode 100644 index 0000000..fb852f6 --- /dev/null +++ b/internal/protocols/dnssec/dnssec.go @@ -0,0 +1,3 @@ +package dnssec + +// DNSSEC resolver implementation diff --git a/internal/protocols/do53/do53.go b/internal/protocols/do53/do53.go new file mode 100644 index 0000000..8728edd --- /dev/null +++ b/internal/protocols/do53/do53.go @@ -0,0 +1,63 @@ +package do53 + +import ( + "fmt" + "net" + + "golang.org/x/net/dns/dnsmessage" +) + +func Run(domain, queryType, dest string, dnssec bool) error { + + message, err := MakeDNSMessage(domain, queryType) + if err != nil { + return err + } + + udpAddr, err := net.ResolveUDPAddr("udp", dest) + if err != nil { + return fmt.Errorf("failed to resolve UDP address: %v", err) + } + + udpConn, err := net.DialUDP("udp", nil, udpAddr) + if err != nil { + return fmt.Errorf("failed to dial UDP connection: %v", err) + } + defer udpConn.Close() + + _, err = udpConn.Write(message) + if err != nil { + return fmt.Errorf("failed to send DNS query: %v", err) + } + + buf := make([]byte, 4096) + n, err := udpConn.Read(buf) + if err != nil { + return fmt.Errorf("failed to read DNS response: %v", err) + } + + var parser dnsmessage.Parser + _, err = parser.Start(buf[:n]) + if err != nil { + return fmt.Errorf("failed to parse DNS response: %v", err) + } + + // TODO: Check if the response had no errors or TD bit set + + err = parser.SkipAllQuestions() + if err != nil { + return fmt.Errorf("failed to skip questions: %v", err) + } + + answers, err := parser.AllAnswers() + if err != nil { + return err + } + + for _, answer := range answers { + fmt.Println(answer.GoString()) + } + + return nil +} + diff --git a/internal/protocols/do53/packet.go b/internal/protocols/do53/packet.go new file mode 100644 index 0000000..d287218 --- /dev/null +++ b/internal/protocols/do53/packet.go @@ -0,0 +1,60 @@ +package do53 + +import ( + "fmt" + + "golang.org/x/net/dns/dnsmessage" +) + +func MakeDNSMessage(domain string, queryType string) ([]byte, error) { + messageHeader := dnsmessage.Header{ + ID: 1234, // FIX: Use a random ID + Response: false, + OpCode: dnsmessage.OpCode(0), + RecursionDesired: true, + } + + messageBuilder := dnsmessage.NewBuilder(nil, messageHeader) + queryName, err := dnsmessage.NewName(domain) + if err != nil { + return nil, fmt.Errorf("failed to create query name: %v", err) + } + + // Determine query type + var queryTypeValue dnsmessage.Type + switch queryType { + case "A": + queryTypeValue = dnsmessage.TypeA + case "AAAA": + queryTypeValue = dnsmessage.TypeAAAA + case "MX": + queryTypeValue = dnsmessage.TypeMX + case "CNAME": + queryTypeValue = dnsmessage.TypeCNAME + case "TXT": + queryTypeValue = dnsmessage.TypeTXT + default: + queryTypeValue = dnsmessage.TypeA + } + + messageQuestion := dnsmessage.Question{ + Name: queryName, + Type: queryTypeValue, + Class: dnsmessage.ClassINET, + } + + err = messageBuilder.StartQuestions() + if err != nil { + return nil, err + } + err = messageBuilder.Question(messageQuestion) + if err != nil { + return nil, fmt.Errorf("failed to add question: %v", err) + } + + message, err := messageBuilder.Finish() + if err != nil { + return nil, fmt.Errorf("failed to build message: %v", err) + } + return message, nil +} diff --git a/internal/protocols/doh/doh.go b/internal/protocols/doh/doh.go new file mode 100644 index 0000000..915bac5 --- /dev/null +++ b/internal/protocols/doh/doh.go @@ -0,0 +1,148 @@ +package doh + +import ( + "bufio" + "bytes" + "crypto/tls" + "fmt" + "io" + "net" + "net/http" + "os" + + "github.com/afonsofrancof/sdns-perf/internal/protocols/do53" + "golang.org/x/net/dns/dnsmessage" +) + +func Run(domain, queryType, server, path, proxy string, dnssec bool) error { + + DNSMessage, err := do53.MakeDNSMessage(domain, queryType) + if err != nil { + return err + } + + // Step 1 - Establish a TCP Connection + tcpConn, err := net.Dial("tcp", server) + if err != nil { + return fmt.Errorf("failed to establish TCP connection: %v", err) + } + defer tcpConn.Close() + + // Step 2 - Upgrade it to a TLS Connection + + // Temporary keylog file to allow traffic inspection + keyLogFile, err := os.OpenFile( + "tls-key-log.txt", + os.O_APPEND|os.O_CREATE|os.O_WRONLY, + 0600, + ) + if err != nil { + return fmt.Errorf("failed opening key log file: %v", err) + } + defer keyLogFile.Close() + + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, + MinVersion: tls.VersionTLS12, + KeyLogWriter: keyLogFile, + } + + tlsConn := tls.Client(tcpConn, tlsConfig) + err = tlsConn.Handshake() + if err != nil { + return fmt.Errorf("failed to execute the TLS handshake: %v", err) + } + defer tlsConn.Close() + + // Step 3 - Create an HTTP request with the do53 message in the body + httpReq, err := http.NewRequest("POST", "https://"+server+"/"+path, bytes.NewBuffer(DNSMessage)) + if err != nil { + return fmt.Errorf("failed to create HTTP request: %v", err) + } + httpReq.Header.Add("Content-Type", "application/dns-message") + httpReq.Header.Set("Accept", "application/dns-message") + + err = httpReq.Write(tlsConn) + if err != nil { + return fmt.Errorf("failed writing HTTP request: %v", err) + } + + reader := bufio.NewReader(tlsConn) + resp, err := http.ReadResponse(reader, httpReq) + if err != nil { + return fmt.Errorf("failed reading HTTP response: %v", err) + } + defer resp.Body.Close() + + responseBody := make([]byte, 4096) + n, err := resp.Body.Read(responseBody) + if err != nil && err != io.EOF { + return fmt.Errorf("failed reading response body: %v", err) + } + + // Parse the response + var parser dnsmessage.Parser + header, err := parser.Start(responseBody[:n]) + if err != nil { + return fmt.Errorf("failed to parse DNS response: %v", err) + } + + fmt.Printf("DNS Response Header:\n") + fmt.Printf(" ID: %d\n", header.ID) + fmt.Printf(" Response: %v\n", header.Response) + fmt.Printf(" RCode: %v\n", header.RCode) + + // Skip all questions before reading answers + err = parser.SkipAllQuestions() + if err != nil { + return fmt.Errorf("failed to skip questions: %v", err) + } + + // Parse answers + fmt.Printf("\nAnswers:\n") + answers, err := parser.AllAnswers() + + for i, answer := range answers { + + if err != nil { + return fmt.Errorf("failed to parse answer %d: %v", i, err) + } + + fmt.Printf(" Answer %d:\n", i+1) + fmt.Printf(" Name: %v\n", answer.Header.Name) + fmt.Printf(" Type: %v\n", answer.Header.Type) + fmt.Printf(" TTL: %v seconds\n", answer.Header.TTL) + + // Handle different record types + switch answer.Header.Type { + case dnsmessage.TypeA: + if r, ok := answer.Body.(*dnsmessage.AResource); ok { + fmt.Printf(" IPv4: %d.%d.%d.%d\n", r.A[0], r.A[1], r.A[2], r.A[3]) + } + case dnsmessage.TypeAAAA: + if r, ok := answer.Body.(*dnsmessage.AAAAResource); ok { + ip := r.AAAA + fmt.Printf(" IPv6: %02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x\n", + ip[0], ip[1], ip[2], ip[3], ip[4], ip[5], ip[6], ip[7], + ip[8], ip[9], ip[10], ip[11], ip[12], ip[13], ip[14], ip[15]) + } + case dnsmessage.TypeCNAME: + if r, ok := answer.Body.(*dnsmessage.CNAMEResource); ok { + fmt.Printf(" CNAME: %v\n", r.CNAME) + } + case dnsmessage.TypeMX: + if r, ok := answer.Body.(*dnsmessage.MXResource); ok { + fmt.Printf(" Preference: %v\n", r.Pref) + fmt.Printf(" MX: %v\n", r.MX) + } + case dnsmessage.TypeTXT: + if r, ok := answer.Body.(*dnsmessage.TXTResource); ok { + fmt.Printf(" TXT: %v\n", r.TXT) + } + default: + fmt.Printf(" [Unsupported record type]\n") + } + } + + return nil +} diff --git a/internal/protocols/doq/doq.go b/internal/protocols/doq/doq.go new file mode 100644 index 0000000..123b02c --- /dev/null +++ b/internal/protocols/doq/doq.go @@ -0,0 +1,3 @@ +package doq + +// DoQ (DNS over QUIC) resolver implementation diff --git a/internal/protocols/dot/dot.go b/internal/protocols/dot/dot.go new file mode 100644 index 0000000..d1f5d05 --- /dev/null +++ b/internal/protocols/dot/dot.go @@ -0,0 +1,3 @@ +package dot + +// DoT (DNS over TLS) resolver implementation