From 7541763c24ba6f08003447e7b4a4935e1eadce28 Mon Sep 17 00:00:00 2001 From: afonso Date: Sat, 31 Aug 2024 11:36:28 +0100 Subject: [PATCH] First commit --- doh-proxy.go | 206 +++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 16 ++++ go.sum | 14 ++++ 3 files changed, 236 insertions(+) create mode 100644 doh-proxy.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/doh-proxy.go b/doh-proxy.go new file mode 100644 index 0000000..da5fb33 --- /dev/null +++ b/doh-proxy.go @@ -0,0 +1,206 @@ +package main + +import ( + "bytes" + "crypto/tls" + "flag" + "io" + "log" + "net" + "net/http" + "os" + "time" + + "github.com/miekg/dns" + "golang.org/x/net/http2" +) + +// ProtocolType represents the type of DNS protocol +type ProtocolType int + +const ( + // Enum-like constants for ProtocolType + TCP ProtocolType = iota + UDP +) + +// DoHProxy represents the DNS-over-HTTPS proxy +type DoHProxy struct { + listenAddress string + port string + upstreamURLs []string + client *http.Client + protocols []ProtocolType // List of protocols to listen on + logRequests bool +} + +// NewDoHProxy initializes a new DoHProxy instance +func NewDoHProxy(listenAddress, port string, upstreamURLs []string, protocols []ProtocolType, logRequests bool) *DoHProxy { + // HTTP client with support for HTTP/2 + transport := &http.Transport{ + TLSClientConfig: &tls.Config{}, + } + + http2.ConfigureTransport(transport) // Enable HTTP/2 support + + // HTTP client for DoH requests + client := &http.Client{ + Transport: transport, + Timeout: 5 * time.Second, // Set a timeout for requests + } + + return &DoHProxy{ + listenAddress: listenAddress, + port: port, + upstreamURLs: upstreamURLs, + client: client, + protocols: protocols, + logRequests: logRequests, + } +} + +// HandleDNSRequest handles incoming DNS requests and forwards them to DoH servers +func (p *DoHProxy) HandleDNSRequest(w dns.ResponseWriter, r *dns.Msg) { + dnsQuery, err := r.Pack() + if err != nil { + log.Printf("Failed to pack DNS request: %v", err) + return + } + + var response *http.Response + var currentUpstream string + + // Send the DNS query to each upstream DoH server until one succeeds + for _, upstream := range p.upstreamURLs { + req, _ := http.NewRequest("POST", upstream, bytes.NewBuffer(dnsQuery)) + req.Header.Set("Content-Type", "application/dns-message") + req.Header.Set("Accept", "application/dns-message") + + response, err = p.client.Do(req) + if err == nil && response.StatusCode == http.StatusOK { + currentUpstream = upstream + break + } + } + if currentUpstream == "" { + if p.logRequests { + log.Printf("Failed to query any upstream") + } + return + } + if response == nil { + log.Printf("All upstream DoH servers failed") + dns.HandleFailed(w, r) + return + } + defer response.Body.Close() + + // Read the response body + body, err := io.ReadAll(response.Body) + if err != nil { + log.Printf("Failed to read response from upstream: %v", err) + dns.HandleFailed(w, r) + return + } + + // Unpack the DNS response and send it back to the client + dnsResponse := new(dns.Msg) + if err := dnsResponse.Unpack(body); err != nil { + log.Printf("Failed to unpack DNS response: %v", err) + dns.HandleFailed(w, r) + return + } + if p.logRequests { + log.Println("Successfully proxied request through ", currentUpstream) + } + w.WriteMsg(dnsResponse) + +} + +// Run starts the DNS server and listens for incoming queries based on the protocol +func (p *DoHProxy) Run() { + for _, proto := range p.protocols { + switch proto { + case TCP: + go func() { + server := &dns.Server{Addr: net.JoinHostPort(p.listenAddress, p.port), Net: "tcp"} + dns.HandleFunc(".", p.HandleDNSRequest) + + log.Printf("Starting DoH Proxy on %s:%s over TCP", p.listenAddress, p.port) + if err := server.ListenAndServe(); err != nil { + log.Fatalf("Failed to start DNS server (TCP): %v", err) + } + }() + case UDP: + go func() { + server := &dns.Server{Addr: net.JoinHostPort(p.listenAddress, p.port), Net: "udp"} + dns.HandleFunc(".", p.HandleDNSRequest) + + log.Printf("Starting DoH Proxy on %s:%s over UDP", p.listenAddress, p.port) + if err := server.ListenAndServe(); err != nil { + log.Fatalf("Failed to start DNS server (UDP): %v", err) + } + }() + } + } + + // Keep the main goroutine running indefinitely + select {} +} + +func main() { + // Define flags using the flag library + listenAddress := flag.String("l", "127.0.0.1", "Listen address for the DNS server") + port := flag.String("p", "53", "Port for the DNS server") + + // Define flags for protocols + tcpFlag := flag.Bool("tcp", false, "Listen on TCP") + udpFlag := flag.Bool("udp", false, "Listen on UDP") + + // Define flag for logging + logFlag := flag.Bool("log", false, "Log each request proxied through an upstream") + + var upstreamURLs []string + + // Custom flag for handling multiple upstream URLs + flag.Func("u", `Upstream DoH server URL (can be specified multiple times) +Example: + -u https://dns.quad9.net/dns-query -u https://1.1.1.1/dns-query +WARNING: + If this is your system's default DNS resolver + and the server URL is a domain name, at least one other + DNS server after this one must be specified as an IP address + in order to resolve the domain name of the first one.`, func(value string) error { + log.Printf("Added %s as an upstream DoH server\n", value) + upstreamURLs = append(upstreamURLs, value) + return nil + }) + + // Parse the flags + flag.Parse() + + // Check if at least one upstream DoH URL is provided + if len(upstreamURLs) == 0 { + flag.PrintDefaults() + os.Exit(1) + } + + // Determine which protocols to use + var protocols []ProtocolType + if *tcpFlag { + protocols = append(protocols, TCP) + } + if *udpFlag { + protocols = append(protocols, UDP) + } + if !*tcpFlag && !*udpFlag { + // Default to both if no specific flag is provided + protocols = []ProtocolType{TCP, UDP} + } + if *logFlag { + log.Println("Logging requests") + } + // Initialize and run the DoH proxy + proxy := NewDoHProxy(*listenAddress, *port, upstreamURLs, protocols, *logFlag) + proxy.Run() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..559ec11 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module dohproxy + +go 1.23.0 + +require ( + github.com/miekg/dns v1.1.62 + golang.org/x/net v0.28.0 +) + +require ( + golang.org/x/mod v0.18.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect + golang.org/x/tools v0.22.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1fabb37 --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= +github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=