mirror of
https://github.com/folbricht/routedns.git
synced 2026-01-06 09:40:03 -06:00
Support setting source address of resolvers (#75)
* Support setting source address of resolvers * Support LocalAddr option in UDP and TCP resolvers * Support local-address option for DoQ and DoT
This commit is contained in:
@@ -39,6 +39,7 @@ type resolver struct {
|
||||
ClientKey string `toml:"client-key"`
|
||||
ClientCrt string `toml:"client-crt"`
|
||||
BootstrapAddr string `toml:"bootstrap-address"`
|
||||
LocalAddr string `toml:"local-address"`
|
||||
}
|
||||
|
||||
// DoH-specific resolver options
|
||||
|
||||
@@ -12,8 +12,3 @@ ca = "example-config/server.crt"
|
||||
address = "127.0.0.1:53"
|
||||
protocol = "udp"
|
||||
resolver = "cloudflare-doh-quic"
|
||||
|
||||
[listeners.local-tcp]
|
||||
address = "127.0.0.1:53"
|
||||
protocol = "tcp"
|
||||
resolver = "cloudflare-doh-quic"
|
||||
|
||||
@@ -10,7 +10,3 @@ address = "127.0.0.1:53"
|
||||
protocol = "udp"
|
||||
resolver = "cloudflare-doh-quic"
|
||||
|
||||
[listeners.local-tcp]
|
||||
address = "127.0.0.1:53"
|
||||
protocol = "tcp"
|
||||
resolver = "cloudflare-doh-quic"
|
||||
|
||||
@@ -11,8 +11,3 @@ bootstrap-address = "127.0.0.1"
|
||||
address = "127.0.0.1:53"
|
||||
protocol = "udp"
|
||||
resolver = "local-doq"
|
||||
|
||||
[listeners.local-tcp]
|
||||
address = "127.0.0.1:53"
|
||||
protocol = "tcp"
|
||||
resolver = "local-doq"
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
@@ -80,6 +81,7 @@ func start(opt options, args []string) error {
|
||||
}
|
||||
opt := rdns.DoQClientOptions{
|
||||
BootstrapAddr: r.BootstrapAddr,
|
||||
LocalAddr: net.ParseIP(r.LocalAddr),
|
||||
TLSConfig: tlsConfig,
|
||||
}
|
||||
resolvers[id], err = rdns.NewDoQClient(id, r.Address, opt)
|
||||
@@ -93,6 +95,7 @@ func start(opt options, args []string) error {
|
||||
}
|
||||
opt := rdns.DoTClientOptions{
|
||||
BootstrapAddr: r.BootstrapAddr,
|
||||
LocalAddr: net.ParseIP(r.LocalAddr),
|
||||
TLSConfig: tlsConfig,
|
||||
}
|
||||
resolvers[id], err = rdns.NewDoTClient(id, r.Address, opt)
|
||||
@@ -106,6 +109,7 @@ func start(opt options, args []string) error {
|
||||
}
|
||||
opt := rdns.DTLSClientOptions{
|
||||
BootstrapAddr: r.BootstrapAddr,
|
||||
LocalAddr: net.ParseIP(r.LocalAddr),
|
||||
DTLSConfig: dtlsConfig,
|
||||
}
|
||||
resolvers[id], err = rdns.NewDTLSClient(id, r.Address, opt)
|
||||
@@ -122,15 +126,17 @@ func start(opt options, args []string) error {
|
||||
TLSConfig: tlsConfig,
|
||||
BootstrapAddr: r.BootstrapAddr,
|
||||
Transport: r.Transport,
|
||||
LocalAddr: net.ParseIP(r.LocalAddr),
|
||||
}
|
||||
resolvers[id], err = rdns.NewDoHClient(id, r.Address, opt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse resolver config for '%s' : %s", id, err)
|
||||
}
|
||||
case "tcp":
|
||||
resolvers[id] = rdns.NewDNSClient(id, r.Address, "tcp")
|
||||
case "udp":
|
||||
resolvers[id] = rdns.NewDNSClient(id, r.Address, "udp")
|
||||
case "tcp", "udp":
|
||||
opt := rdns.DNSClientOptions{
|
||||
LocalAddr: net.ParseIP(r.LocalAddr),
|
||||
}
|
||||
resolvers[id] = rdns.NewDNSClient(id, r.Address, r.Protocol, opt)
|
||||
default:
|
||||
return fmt.Errorf("unsupported protocol '%s' for resolver '%s'", r.Protocol, id)
|
||||
}
|
||||
|
||||
24
dnsclient.go
24
dnsclient.go
@@ -2,6 +2,7 @@ package rdns
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -15,18 +16,35 @@ type DNSClient struct {
|
||||
pipeline *Pipeline
|
||||
}
|
||||
|
||||
type DNSClientOptions struct {
|
||||
// Local IP to use for outbound connections. If nil, a local address is chosen.
|
||||
LocalAddr net.IP
|
||||
}
|
||||
|
||||
var _ Resolver = &DNSClient{}
|
||||
|
||||
// NewDNSClient returns a new instance of DNSClient which is a plain DNS resolver
|
||||
// that supports pipelining over a single connection.
|
||||
func NewDNSClient(id, endpoint, net string) *DNSClient {
|
||||
func NewDNSClient(id, endpoint, network string, opt DNSClientOptions) *DNSClient {
|
||||
// Use a custom dialer if a local address was provided
|
||||
var dialer *net.Dialer
|
||||
if opt.LocalAddr != nil {
|
||||
switch network {
|
||||
case "tcp":
|
||||
dialer = &net.Dialer{LocalAddr: &net.TCPAddr{IP: opt.LocalAddr}}
|
||||
case "udp":
|
||||
dialer = &net.Dialer{LocalAddr: &net.UDPAddr{IP: opt.LocalAddr}}
|
||||
}
|
||||
}
|
||||
|
||||
client := &dns.Client{
|
||||
Net: net,
|
||||
Net: network,
|
||||
Dialer: dialer,
|
||||
TLSConfig: &tls.Config{},
|
||||
}
|
||||
return &DNSClient{
|
||||
id: id,
|
||||
net: net,
|
||||
net: network,
|
||||
endpoint: endpoint,
|
||||
pipeline: NewPipeline(endpoint, client),
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func TestDNSClientSimpleTCP(t *testing.T) {
|
||||
d := NewDNSClient("test-dns", "8.8.8.8:53", "tcp")
|
||||
d := NewDNSClient("test-dns", "8.8.8.8:53", "tcp", DNSClientOptions{})
|
||||
q := new(dns.Msg)
|
||||
q.SetQuestion("google.com.", dns.TypeA)
|
||||
r, err := d.Resolve(q, ClientInfo{})
|
||||
@@ -17,7 +17,7 @@ func TestDNSClientSimpleTCP(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDNSClientSimpleUDP(t *testing.T) {
|
||||
d := NewDNSClient("test-dns", "8.8.8.8:53", "udp")
|
||||
d := NewDNSClient("test-dns", "8.8.8.8:53", "udp", DNSClientOptions{})
|
||||
q := new(dns.Msg)
|
||||
q.SetQuestion("google.com.", dns.TypeA)
|
||||
r, err := d.Resolve(q, ClientInfo{})
|
||||
|
||||
@@ -970,6 +970,7 @@ Resolvers are defined in the configuration like so `[resolvers.NAME]` and have t
|
||||
- `address` - Remote server endpoint and port. Can be IP or hostname, or a full URL depending on the protocol. See the [Bootstrapping](#Bootstrapping) on how to handle hostnames that can't be resolved.
|
||||
- `protocol` - The DNS protocol used to send queries, can be `udp`, `tcp`, `dot`, `doh`, `doq`.
|
||||
- `bootstrap-address` - Use this IP address if the name in `address` can't be resolved. Using the IP in `address` directly may not work when TLS/certificates are used by the server.
|
||||
- `local-address` - IP of the local interface to use for outgoing connections. The address is automatically chosen if this option is left blank.
|
||||
|
||||
Secure resolvers such as DoT, DoH, or DoQ offer additional options to configure the TLS connections.
|
||||
|
||||
|
||||
60
dohclient.go
60
dohclient.go
@@ -26,13 +26,16 @@ type DoHClientOptions struct {
|
||||
// Query method, either GET or POST. If empty, POST is used.
|
||||
Method string
|
||||
|
||||
// Bootstrap address - IP to use for the serivce instead of looking up
|
||||
// Bootstrap address - IP to use for the service instead of looking up
|
||||
// the service's hostname with potentially plain DNS.
|
||||
BootstrapAddr string
|
||||
|
||||
// Transport protocol to run HTTPS over. "quic" or "tcp", defaults to "tcp".
|
||||
Transport string
|
||||
|
||||
// Local IP to use for outbound connections. If nil, a local address is chosen.
|
||||
LocalAddr net.IP
|
||||
|
||||
TLSConfig *tls.Config
|
||||
}
|
||||
|
||||
@@ -195,15 +198,17 @@ func dohTcpTransport(opt DoHClientOptions) (http.RoundTripper, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Use a custom dialer if a bootstrap address was provided
|
||||
if opt.BootstrapAddr != "" {
|
||||
var d net.Dialer
|
||||
// Use a custom dialer if a bootstrap address or local address was provided
|
||||
if opt.BootstrapAddr != "" || opt.LocalAddr != nil {
|
||||
d := net.Dialer{LocalAddr: &net.TCPAddr{IP: opt.LocalAddr}}
|
||||
tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
_, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if opt.BootstrapAddr != "" {
|
||||
_, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addr = net.JoinHostPort(opt.BootstrapAddr, port)
|
||||
}
|
||||
addr = net.JoinHostPort(opt.BootstrapAddr, port)
|
||||
return d.DialContext(ctx, network, addr)
|
||||
}
|
||||
}
|
||||
@@ -215,16 +220,16 @@ func dohQuicTransport(opt DoHClientOptions) (http.RoundTripper, error) {
|
||||
TLSClientConfig: opt.TLSConfig,
|
||||
QuicConfig: &quic.Config{},
|
||||
Dial: func(network, addr string, tlsConfig *tls.Config, config *quic.Config) (quic.EarlySession, error) {
|
||||
hostname, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if opt.BootstrapAddr != "" {
|
||||
hostname, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig = tlsConfig.Clone()
|
||||
tlsConfig.ServerName = hostname
|
||||
addr = net.JoinHostPort(opt.BootstrapAddr, port)
|
||||
}
|
||||
return newQuicSession(addr, tlsConfig, config)
|
||||
return newQuicSession(hostname, addr, opt.LocalAddr, tlsConfig, config)
|
||||
},
|
||||
}
|
||||
return tr, nil
|
||||
@@ -237,7 +242,9 @@ func dohQuicTransport(opt DoHClientOptions) (http.RoundTripper, error) {
|
||||
type quicSession struct {
|
||||
quic.Session
|
||||
|
||||
addr string
|
||||
hostname string
|
||||
rAddr string
|
||||
lAddr net.IP
|
||||
tlsConfig *tls.Config
|
||||
config *quic.Config
|
||||
mu sync.Mutex
|
||||
@@ -245,8 +252,8 @@ type quicSession struct {
|
||||
expiredContext context.Context
|
||||
}
|
||||
|
||||
func newQuicSession(addr string, tlsConfig *tls.Config, config *quic.Config) (quic.EarlySession, error) {
|
||||
session, err := quic.DialAddr(addr, tlsConfig, config)
|
||||
func newQuicSession(hostname, rAddr string, lAddr net.IP, tlsConfig *tls.Config, config *quic.Config) (quic.EarlySession, error) {
|
||||
session, err := quicDial(hostname, rAddr, lAddr, tlsConfig, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -254,7 +261,9 @@ func newQuicSession(addr string, tlsConfig *tls.Config, config *quic.Config) (qu
|
||||
cancel()
|
||||
|
||||
return &quicSession{
|
||||
addr: addr,
|
||||
hostname: hostname,
|
||||
rAddr: rAddr,
|
||||
lAddr: lAddr,
|
||||
tlsConfig: tlsConfig,
|
||||
config: config,
|
||||
Session: session,
|
||||
@@ -272,7 +281,8 @@ func (s *quicSession) OpenStreamSync(ctx context.Context) (quic.Stream, error) {
|
||||
stream, err := s.Session.OpenStreamSync(ctx)
|
||||
if err != nil {
|
||||
_ = s.Session.CloseWithError(quic.ErrorCode(DOQNoError), "")
|
||||
s.Session, err = quic.DialAddr(s.addr, s.tlsConfig, s.config)
|
||||
|
||||
s.Session, err = quicDial(s.hostname, s.rAddr, s.lAddr, s.tlsConfig, s.config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -287,7 +297,7 @@ func (s *quicSession) OpenStream() (quic.Stream, error) {
|
||||
stream, err := s.Session.OpenStream()
|
||||
if err != nil {
|
||||
_ = s.Session.CloseWithError(quic.ErrorCode(DOQNoError), "")
|
||||
s.Session, err = quic.DialAddr(s.addr, s.tlsConfig, s.config)
|
||||
s.Session, err = quicDial(s.hostname, s.rAddr, s.lAddr, s.tlsConfig, s.config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -295,3 +305,15 @@ func (s *quicSession) OpenStream() (quic.Stream, error) {
|
||||
}
|
||||
return stream, err
|
||||
}
|
||||
|
||||
func quicDial(hostname, rAddr string, lAddr net.IP, tlsConfig *tls.Config, config *quic.Config) (quic.Session, error) {
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", rAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: lAddr, Port: 0})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return quic.Dial(udpConn, udpAddr, hostname, tlsConfig, config)
|
||||
}
|
||||
|
||||
19
doqclient.go
19
doqclient.go
@@ -36,6 +36,9 @@ type DoQClientOptions struct {
|
||||
// the service's hostname with potentially plain DNS.
|
||||
BootstrapAddr string
|
||||
|
||||
// Local IP to use for outbound connections. If nil, a local address is chosen.
|
||||
LocalAddr net.IP
|
||||
|
||||
TLSConfig *tls.Config
|
||||
}
|
||||
|
||||
@@ -52,11 +55,11 @@ func NewDoQClient(id, endpoint string, opt DoQClientOptions) (*DoQClient, error)
|
||||
// hostname in the TLS handshake. The library doesn't support custom dialers, so
|
||||
// instead set the ServerName in the TLS config to the name in the endpoint config, and
|
||||
// replace the name in the endpoint with the bootstrap IP.
|
||||
host, port, err := net.SplitHostPort(endpoint)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse dot endpoint '%s'", endpoint)
|
||||
}
|
||||
if opt.BootstrapAddr != "" {
|
||||
host, port, err := net.SplitHostPort(endpoint)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse dot endpoint '%s'", endpoint)
|
||||
}
|
||||
opt.TLSConfig.ServerName = host
|
||||
endpoint = net.JoinHostPort(opt.BootstrapAddr, port)
|
||||
}
|
||||
@@ -68,7 +71,9 @@ func NewDoQClient(id, endpoint string, opt DoQClientOptions) (*DoQClient, error)
|
||||
requests: make(chan *request),
|
||||
log: log,
|
||||
session: doqSession{
|
||||
hostname: host,
|
||||
endpoint: endpoint,
|
||||
lAddr: opt.LocalAddr,
|
||||
tlsConfig: opt.TLSConfig,
|
||||
log: log,
|
||||
},
|
||||
@@ -151,7 +156,9 @@ func (d *DoQClient) String() string {
|
||||
}
|
||||
|
||||
type doqSession struct {
|
||||
hostname string
|
||||
endpoint string
|
||||
lAddr net.IP
|
||||
tlsConfig *tls.Config
|
||||
log *logrus.Entry
|
||||
|
||||
@@ -167,7 +174,7 @@ func (s *doqSession) getStream() (quic.Stream, error) {
|
||||
// If we don't have a session yet, make one
|
||||
if s.session == nil {
|
||||
var err error
|
||||
s.session, err = quic.DialAddr(s.endpoint, s.tlsConfig, nil)
|
||||
s.session, err = quicDial(s.hostname, s.endpoint, s.lAddr, s.tlsConfig, nil)
|
||||
if err != nil {
|
||||
s.log.WithError(err).Error("failed to open session")
|
||||
return nil, err
|
||||
@@ -178,7 +185,7 @@ func (s *doqSession) getStream() (quic.Stream, error) {
|
||||
if err != nil {
|
||||
// Try to open a new session
|
||||
_ = s.session.CloseWithError(quic.ErrorCode(DOQNoError), "")
|
||||
s.session, err = quic.DialAddr(s.endpoint, s.tlsConfig, nil)
|
||||
s.session, err = quicDial(s.hostname, s.endpoint, s.lAddr, s.tlsConfig, nil)
|
||||
if err != nil {
|
||||
s.log.WithError(err).Error("failed to open session")
|
||||
return nil, err
|
||||
|
||||
@@ -22,6 +22,9 @@ type DoTClientOptions struct {
|
||||
// the service's hostname with potentially plain DNS.
|
||||
BootstrapAddr string
|
||||
|
||||
// Local IP to use for outbound connections. If nil, a local address is chosen.
|
||||
LocalAddr net.IP
|
||||
|
||||
TLSConfig *tls.Config
|
||||
}
|
||||
|
||||
@@ -29,9 +32,15 @@ var _ Resolver = &DoTClient{}
|
||||
|
||||
// NewDoTClient instantiates a new DNS-over-TLS resolver.
|
||||
func NewDoTClient(id, endpoint string, opt DoTClientOptions) (*DoTClient, error) {
|
||||
// Use a custom dialer if a local address was provided
|
||||
var dialer *net.Dialer
|
||||
if opt.LocalAddr != nil {
|
||||
dialer = &net.Dialer{LocalAddr: &net.TCPAddr{IP: opt.LocalAddr}}
|
||||
}
|
||||
client := &dns.Client{
|
||||
Net: "tcp-tls",
|
||||
TLSConfig: opt.TLSConfig,
|
||||
Dialer: dialer,
|
||||
}
|
||||
// If a bootstrap address was provided, we need to use the IP for the connection but the
|
||||
// hostname in the TLS handshake. The DNS library doesn't support custom dialers, so
|
||||
|
||||
@@ -80,7 +80,7 @@ func TestDoTListenerMutual(t *testing.T) {
|
||||
|
||||
func TestDoTListenerPadding(t *testing.T) {
|
||||
// Define a listener that does not respond with padding
|
||||
upstream := NewDNSClient("test-dns", "8.8.8.8:53", "udp")
|
||||
upstream := NewDNSClient("test-dns", "8.8.8.8:53", "udp", DNSClientOptions{})
|
||||
|
||||
// Find a free port for the listener
|
||||
addr, err := getLnAddress()
|
||||
|
||||
@@ -23,6 +23,9 @@ type DTLSClientOptions struct {
|
||||
// the service's hostname with potentially plain DNS.
|
||||
BootstrapAddr string
|
||||
|
||||
// Local IP to use for outbound connections. If nil, a local address is chosen.
|
||||
LocalAddr net.IP
|
||||
|
||||
DTLSConfig *dtls.Config
|
||||
}
|
||||
|
||||
@@ -59,8 +62,14 @@ func NewDTLSClient(id, endpoint string, opt DTLSClientOptions) (*DTLSClient, err
|
||||
}
|
||||
addr := &net.UDPAddr{IP: ip, Port: p}
|
||||
|
||||
var laddr *net.UDPAddr
|
||||
if opt.LocalAddr != nil {
|
||||
laddr = &net.UDPAddr{IP: opt.LocalAddr}
|
||||
}
|
||||
|
||||
client := &dtlsDialer{
|
||||
addr: addr,
|
||||
raddr: addr,
|
||||
laddr: laddr,
|
||||
dtlsConfig: opt.DTLSConfig,
|
||||
}
|
||||
return &DTLSClient{
|
||||
@@ -87,11 +96,16 @@ func (d *DTLSClient) String() string {
|
||||
}
|
||||
|
||||
type dtlsDialer struct {
|
||||
addr *net.UDPAddr
|
||||
raddr *net.UDPAddr
|
||||
laddr *net.UDPAddr
|
||||
dtlsConfig *dtls.Config
|
||||
}
|
||||
|
||||
func (d dtlsDialer) Dial(address string) (*dns.Conn, error) {
|
||||
c, err := dtls.Dial("udp", d.addr, d.dtlsConfig)
|
||||
pConn, err := net.DialUDP("udp", d.laddr, d.raddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c, err := dtls.Client(pConn, d.dtlsConfig)
|
||||
return &dns.Conn{Conn: c}, err
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ func Example_resolver() {
|
||||
|
||||
func Example_group() {
|
||||
// Define resolvers
|
||||
r1 := rdns.NewDNSClient("google1", "8.8.8.8:53", "udp")
|
||||
r2 := rdns.NewDNSClient("google2", "8.8.4.4:53", "udp")
|
||||
r1 := rdns.NewDNSClient("google1", "8.8.8.8:53", "udp", rdns.DNSClientOptions{})
|
||||
r2 := rdns.NewDNSClient("google2", "8.8.4.4:53", "udp", rdns.DNSClientOptions{})
|
||||
|
||||
// Combine them int a group that does round-robin over the two resolvers
|
||||
g := rdns.NewRoundRobin("test-rr", r1, r2)
|
||||
@@ -39,8 +39,8 @@ func Example_group() {
|
||||
|
||||
func Example_router() {
|
||||
// Define resolvers
|
||||
google := rdns.NewDNSClient("g-dns", "8.8.8.8:53", "udp")
|
||||
cloudflare := rdns.NewDNSClient("cf-dns", "1.1.1.1:53", "udp")
|
||||
google := rdns.NewDNSClient("g-dns", "8.8.8.8:53", "udp", rdns.DNSClientOptions{})
|
||||
cloudflare := rdns.NewDNSClient("cf-dns", "1.1.1.1:53", "udp", rdns.DNSClientOptions{})
|
||||
|
||||
// Build a router that will send all "*.cloudflare.com" to the cloudflare
|
||||
// resolvber while everything else goes to the google resolver (default)
|
||||
|
||||
Reference in New Issue
Block a user