First commit
This commit is contained in:
commit
7541763c24
3 changed files with 236 additions and 0 deletions
206
doh-proxy.go
Normal file
206
doh-proxy.go
Normal file
|
@ -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()
|
||||
}
|
16
go.mod
Normal file
16
go.mod
Normal file
|
@ -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
|
||||
)
|
14
go.sum
Normal file
14
go.sum
Normal file
|
@ -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=
|
Loading…
Reference in a new issue