Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/routedns/example-config/doh-quic-client.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# DNS-over-HTTPS using the QUIC protocol.
# New connections get initiated with 0-RTT if possible.
# enable-0rtt will overwrite the method to GET.

[resolvers.cloudflare-doh-quic]
address = "https://cloudflare-dns.com/dns-query"
address = "https://cloudflare-dns.com/dns-query{?dns}"
doh = { method = "GET" }
protocol = "doh"
transport = "quic"
enable-0rtt = true
Expand Down
1 change: 1 addition & 0 deletions cmd/routedns/example-config/doq-client-simple.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[resolvers.adguard-doq]
address = "dns-unfiltered.adguard.com:8853"
protocol = "doq"
enable-0rtt = true

[listeners.local-udp]
address = "127.0.0.1:53"
Expand Down
220 changes: 36 additions & 184 deletions dohclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"net"
"net/http"
"net/url"
"sync"
"time"

"github.com/quic-go/quic-go"
Expand Down Expand Up @@ -49,28 +48,6 @@ type DoHClientOptions struct {
Use0RTT bool
}

// Returns an HTTP client based on the DoH options
func (opt DoHClientOptions) client(endpoint string) (*http.Client, error) {
var (
tr http.RoundTripper
err error
)
switch opt.Transport {
case "tcp", "":
tr, err = dohTcpTransport(opt)
case "quic":
tr, err = dohQuicTransport(endpoint, opt)
default:
err = fmt.Errorf("unknown protocol: '%s'", opt.Transport)
}
if err != nil {
return nil, err
}
return &http.Client{
Transport: tr,
}, nil
}

// DoHClient is a DNS-over-HTTP resolver with support fot HTTP/2.
type DoHClient struct {
id string
Expand All @@ -90,17 +67,28 @@ func NewDoHClient(id, endpoint string, opt DoHClientOptions) (*DoHClient, error)
return nil, err
}

client, err := opt.client(endpoint)
if err != nil {
return nil, err
// Configure the HTTP Client and Transport based on connection options
var client *http.Client
switch opt.Transport {
case "tcp", "":
tr, err := dohTcpTransport(opt)
if err != nil {
return nil, err
}
client = &http.Client{Transport: tr}
case "quic":
tr, err := dohQuicTransport(endpoint, opt)
if err != nil {
return nil, err
}
client = &http.Client{Transport: tr}
default:
return nil, fmt.Errorf("unknown protocol: '%s'", opt.Transport)
}

if opt.Method == "" {
opt.Method = "POST"
}
if opt.Use0RTT && opt.Transport == "quic" {
opt.Method = "GET"
}
if opt.Method != "POST" && opt.Method != "GET" {
return nil, fmt.Errorf("unsupported method '%s'", opt.Method)
}
Expand Down Expand Up @@ -304,18 +292,32 @@ func dohQuicTransport(endpoint string, opt DoHClientOptions) (http.RoundTripper,
lAddr = opt.LocalAddr
}

dialer := func(ctx context.Context, addr string, tlsConfig *tls.Config, config *quic.Config) (quic.EarlyConnection, error) {
return newQuicConnection(u.Hostname(), addr, lAddr, tlsConfig, config, opt.Use0RTT)
// Initialize the local UDP connection, it'll be re-used for all connections
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: lAddr, Port: 0})
if err != nil {
Log.Error("couldn't listen on UDP socket on local address", "error", err, "local", lAddr.String())
return nil, err
}
quicTransport := &quic.Transport{Conn: udpConn}

dialFunc := quicTransport.Dial
if opt.Use0RTT {
dialFunc = quicTransport.DialEarly
}
if opt.BootstrapAddr != "" {
dialer = func(ctx context.Context, addr string, tlsConfig *tls.Config, config *quic.Config) (quic.EarlyConnection, error) {

dialer := func(ctx context.Context, addr string, tlsConfig *tls.Config, config *quic.Config) (*quic.Conn, error) {
if opt.BootstrapAddr != "" {
_, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
addr = net.JoinHostPort(opt.BootstrapAddr, port)
return newQuicConnection(u.Hostname(), addr, lAddr, tlsConfig, config, opt.Use0RTT)
}
rAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, err
}
return dialFunc(ctx, rAddr, tlsConfig, config)
}

tr := &http3.Transport{
Expand All @@ -327,153 +329,3 @@ func dohQuicTransport(endpoint string, opt DoHClientOptions) (http.RoundTripper,
}
return tr, nil
}

// QUIC connection that automatically restarts when it's used after having timed out. Needed
// since the quic-go RoundTripper doesn't have any connection management and timed out
// connections aren't restarted. This one uses EarlyConnection so we can use 0-RTT if the
// server supports it (lower latency)
type quicConnection struct {
quic.EarlyConnection

hostname string
rAddr string
lAddr net.IP
tlsConfig *tls.Config
config *quic.Config
mu sync.Mutex
udpConn *net.UDPConn
Use0RTT bool
}

func newQuicConnection(hostname, rAddr string, lAddr net.IP, tlsConfig *tls.Config, config *quic.Config, use0RTT bool) (quic.EarlyConnection, error) {
connection, udpConn, err := quicDial(context.TODO(), rAddr, lAddr, tlsConfig, config, use0RTT)
if err != nil {
return nil, err
}

Log.Debug("new quic connection",
slog.String("protocol", "quic"),
slog.String("hostname", hostname),
slog.String("remote", rAddr),
slog.String("local", lAddr.String()),
)

return &quicConnection{
hostname: hostname,
rAddr: rAddr,
lAddr: lAddr,
tlsConfig: tlsConfig,
config: config,
udpConn: udpConn,
EarlyConnection: connection,
Use0RTT: use0RTT,
}, nil
}

func (s *quicConnection) OpenStreamSync(ctx context.Context) (quic.Stream, error) {
s.mu.Lock()
defer s.mu.Unlock()
stream, err := s.EarlyConnection.OpenStreamSync(ctx)
if netErr, ok := err.(net.Error); ok && (netErr.Timeout() || netErr.Temporary()) {
Log.Debug("temporary fail when trying to open stream, attempting new connection", "error", err)
if err = quicRestart(s); err != nil {
return nil, err
}
stream, err = s.EarlyConnection.OpenStreamSync(ctx)
}
return stream, err
}

func (s *quicConnection) OpenStream() (quic.Stream, error) {
s.mu.Lock()
defer s.mu.Unlock()
stream, err := s.EarlyConnection.OpenStream()
if netErr, ok := err.(net.Error); ok && (netErr.Timeout() || netErr.Temporary()) {
Log.Debug("temporary fail when trying to open stream, attempting new connection", "error", err)
if err = quicRestart(s); err != nil {
return nil, err
}
stream, err = s.EarlyConnection.OpenStream()
}
return stream, err
}

func (s *quicConnection) NextConnection(context.Context) (quic.Connection, error) {
return nil, errors.New("not implemented")
}

func quicRestart(s *quicConnection) error {
// Try to open a new connection, but clean up our mess before we do so
// This function should be called with the quicConnection locked, but lock checking isn't provided
// in golang; the issue was closed with "Won't fix"
_ = s.EarlyConnection.CloseWithError(DOQNoError, "")

// We need to close the UDP socket ourselves as we own the socket not the quic-go module
// c.f. https://github.com/quic-go/quic-go/issues/1457
if s.udpConn != nil {
_ = s.udpConn.Close()
s.udpConn = nil
}
Log.Debug("attempt reconnect", slog.String("protocol", "quic"),
slog.String("hostname", s.hostname),
slog.String("local", s.lAddr.String()),
slog.String("remote", s.rAddr),
)
var err error
var earlyConn quic.EarlyConnection
earlyConn, s.udpConn, err = quicDial(context.TODO(), s.rAddr, s.lAddr, s.tlsConfig, s.config, s.Use0RTT)
if err != nil || s.udpConn == nil {
Log.Warn("couldn't restart quic connection", slog.Group("details", slog.String("protocol", "quic"), slog.String("address", s.hostname), slog.String("local", s.lAddr.String())), "error", err)
return err
}
Log.Debug("restarted quic connection", slog.Group("details", slog.String("protocol", "quic"), slog.String("address", s.hostname), slog.String("local", s.lAddr.String()), slog.String("rAddr", s.rAddr)))

s.EarlyConnection = earlyConn
return nil
}

func quicDial(ctx context.Context, rAddr string, lAddr net.IP, tlsConfig *tls.Config, config *quic.Config, use0RTT bool) (quic.EarlyConnection, *net.UDPConn, error) {
var earlyConn quic.EarlyConnection
udpAddr, err := net.ResolveUDPAddr("udp", rAddr)
if err != nil {
Log.Error("couldn't resolve remote addr for UDP quic client", "error", err, "rAddr", rAddr)
return nil, nil, err
}
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: lAddr, Port: 0})
if err != nil {
Log.Error("couldn't listen on UDP socket on local address", "error", err, "local", lAddr.String())
return nil, nil, err
}

if use0RTT {
earlyConn, err = quic.DialEarly(ctx, udpConn, udpAddr, tlsConfig, config)
if err != nil {
_ = udpConn.Close()
Log.Warn("couldn't dial quic early connection", "error", err)
return nil, nil, err
}
} else {
conn, err := quic.Dial(ctx, udpConn, udpAddr, tlsConfig, config)
if err != nil {
_ = udpConn.Close()
Log.Warn("couldn't dial quic connection", "error", err)
return nil, nil, err
}
earlyConn = &earlyConnWrapper{Connection: conn}
}
return earlyConn, udpConn, nil
}

type earlyConnWrapper struct {
quic.Connection
}

func (e *earlyConnWrapper) HandshakeComplete() <-chan struct{} {
ch := make(chan struct{})
close(ch)
return ch
}

func (e *earlyConnWrapper) NextConnection(ctx context.Context) (quic.Connection, error) {
return nil, fmt.Errorf("NextConnection not supported for non-0RTT connections")
}
Loading
Loading