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:
Frank Olbricht
2020-07-21 07:12:09 -06:00
committed by GitHub
parent 4596fbad3e
commit f9cb6aab39
14 changed files with 120 additions and 56 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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)
}

View File

@@ -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),
}

View File

@@ -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{})

View File

@@ -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.

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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)