mirror of
https://github.com/coredns/coredns.git
synced 2026-07-10 09:40:12 -04:00
feat(forward): add source_address directive (#8011)
* fix(dnssec): avoid caching empty signing results (#7996) Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * save only Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * do #8008 Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * Fix Address used if tcp Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * Fix bad using of dialer type Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * core: Add full TSIG verification in gRPC transport (#8006) * core: Add full TSIG verification in gRPC transport This PR add full TSIG verification in gRPC using dns.TsigVerify() so invalid signatures and timestamps are correctly detected instead of only checking key presence. Signed-off-by: Yong Tang <yong.tang.github@outlook.com> * Fix Signed-off-by: Yong Tang <yong.tang.github@outlook.com> * Fix Signed-off-by: Yong Tang <yong.tang.github@outlook.com> --------- Signed-off-by: Yong Tang <yong.tang.github@outlook.com> Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * core: Add full TSIG verification in QUIC transport (#8007) * core: Add full TSIG verification in QUIC transport This PR add full TSIG verification in QUIC using dns.TsigVerify() Signed-off-by: Yong Tang <yong.tang.github@outlook.com> * Fix Signed-off-by: Yong Tang <yong.tang.github@outlook.com> --------- Signed-off-by: Yong Tang <yong.tang.github@outlook.com> Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * fix(test): deduplicate TSIG test helpers (#8009) Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * fix(dnssec): return nil sigs on sign error (#7999) Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * fix(dnssec): return nil from ParseKeyFile on error (#8000) Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * fix(dnsserver): allow view server blocks in any declaration order (#8001) When using the view plugin, filtered and unfiltered server blocks can share the same zone and port. The zone overlap validation rejected this configuration when the unfiltered block was not declared last, because filtered configs treated an already-registered zone as an error. Skip the 'already defined' check for configs that have filter functions, since they are expected to coexist with an unfiltered catch-all block on the same zone/port. Fixes #7733 Signed-off-by: umut-polat <52835619+umut-polat@users.noreply.github.com> Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * fix(doh): use per-connection local address for PROXY protocol (#8005) Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * fix(transfer): batch AXFR records by message size instead of count (#8002) Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * fix(tls): use temp dir for keylog test path (#8010) Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * Rename local_address option to source_address Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * Rename local_address also in readme Add test Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * Resolve change request in pr Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * fix ci lint Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * Improve doc on source_address routing needs Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * Remove added timeout Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * add use of source address also for health check query Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * remove untrailing newline from health_test.go Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * fix file format Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * Update plugin/forward/setup_test.go Co-authored-by: Ville Vesilehto <ville@vesilehto.fi> Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * Remove dead code in TestHealthLocalAddress Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * Fix misspelling Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * Try to set default timeout Signed-off-by: Filippo <filippo.ferrazini@gmail.com> * fix format in health.go Signed-off-by: Filippo <filippo.ferrazini@gmail.com> --------- Signed-off-by: Filippo <filippo.ferrazini@gmail.com> Signed-off-by: Yong Tang <yong.tang.github@outlook.com> Signed-off-by: umut-polat <52835619+umut-polat@users.noreply.github.com> Co-authored-by: Ville Vesilehto <ville@vesilehto.fi> Co-authored-by: Yong Tang <yong.tang.github@outlook.com> Co-authored-by: Umut Polat <52835619+umut-polat@users.noreply.github.com> Co-authored-by: Cedric Wang <wangzongqi@msn.com>
This commit is contained in:
@@ -59,6 +59,7 @@ forward FROM TO... {
|
||||
next RCODE_1 [RCODE_2] [RCODE_3...]
|
||||
failfast_all_unhealthy_upstreams
|
||||
failover RCODE_1 [RCODE_2] [RCODE_3...]
|
||||
source_address IP
|
||||
resolver IP[:PORT] [IP[:PORT]...]
|
||||
}
|
||||
~~~
|
||||
@@ -128,6 +129,7 @@ key exchange mechanisms use the Go `crypto/tls` defaults.
|
||||
* `next_on_nodata` If `NOERROR` is returned by the remote, but an empty answer section (`NODATA`) was provided, execute the next `forward` plugin, if configured.
|
||||
* `failfast_all_unhealthy_upstreams` - determines the handling of requests when all upstream servers are unhealthy and unresponsive to health checks. Enabling this option will immediately return SERVFAIL responses for all requests. By default, requests are sent to a random upstream.
|
||||
* `failover` - By default when a DNS lookup fails to return a DNS response (e.g. timeout), _forward_ will attempt a lookup on the next upstream server. The `failover` option will make _forward_ do the same for any response with a response code matching an `RCODE` ( e.g. `SERVFAIL`、`REFUSED`). `NOERROR` cannot be used. If all upstreams have been tried, the response from the last attempt is returned.
|
||||
* `source_address` **IP** - set the address to use for all outgoing requests as source address (also health check query). This works reliably when upstream servers are reachable from that address. However, if upstream servers belong to different networks, care must be taken. The selected source address may not be valid for all upstreams, and responses may fail if return routing is not properly configured. In such cases, make sure that upstream servers have a route back to the configured source address.
|
||||
* `resolver` **IP[:PORT] [IP[:PORT]...]** specifies one or more DNS resolver addresses used to resolve hostname-based **TO** endpoints at startup. If not specified, the system resolver (`/etc/resolv.conf`) is used. Each address is either a bare IP (IPv4 or IPv6, port 53 assumed) or `IP:port`. Multiple addresses can be specified for redundancy.
|
||||
|
||||
Also note the TLS config is "global" for the whole forwarding proxy if you need a different
|
||||
|
||||
@@ -61,6 +61,7 @@ type Forward struct {
|
||||
failfastUnhealthyUpstreams bool
|
||||
failoverRcodes []int
|
||||
maxConnectAttempts uint32
|
||||
sourceAddress net.IP
|
||||
|
||||
// Hostname resolution fields
|
||||
resolver []string // custom resolver IPs for hostname TO resolution
|
||||
|
||||
@@ -257,6 +257,10 @@ func parseStanza(c *caddy.Controller) (*Forward, error) {
|
||||
f.proxies[i].GetHealthchecker().SetTCPTransport()
|
||||
}
|
||||
f.proxies[i].GetHealthchecker().SetDomain(f.opts.HCDomain)
|
||||
if f.sourceAddress != nil {
|
||||
f.proxies[i].SetLocalAddress(f.sourceAddress)
|
||||
f.proxies[i].GetHealthchecker().SetLocalAddress(f.sourceAddress)
|
||||
}
|
||||
}
|
||||
|
||||
return f, nil
|
||||
@@ -499,6 +503,15 @@ func parseBlock(c *caddy.Controller, f *Forward) error {
|
||||
}
|
||||
}
|
||||
f.resolver = args
|
||||
case "source_address":
|
||||
if !c.NextArg() {
|
||||
return c.ArgErr()
|
||||
}
|
||||
addr := net.ParseIP(c.Val())
|
||||
if addr == nil {
|
||||
return c.Errf("invalid IP address: %s", c.Val())
|
||||
}
|
||||
f.sourceAddress = addr
|
||||
default:
|
||||
return c.Errf("unknown property '%s'", c.Val())
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package forward
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
@@ -92,6 +93,45 @@ func TestSetup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceAddress(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expectedSourceAddress net.IP
|
||||
expectedErr string
|
||||
}{
|
||||
|
||||
{"forward . 127.0.0.1 {\nsource_address 192.0.2.1\n}\n", net.ParseIP("192.0.2.1"), ""},
|
||||
{"forward . 127.0.0.1 {\nsource_address not-an-ip\n}\n", nil, "invalid IP address"},
|
||||
{"forward . 127.0.0.1 {\nsource_address 2001:0db8:85a3:0000:1319:8a2e:0370:7344\n}\n", net.ParseIP("2001:0db8:85a3:0000:1319:8a2e:0370:7344"), ""},
|
||||
{"forward . 127.0.0.1 {\nsource_address ::ffff:192.0.2.1\n}\n", net.ParseIP("192.0.2.1"), ""},
|
||||
{"forward . 127.0.0.1 {\nsource_address \n}\n", nil, "Error during parsing: Wrong argument count or unexpected line ending after 'source_address'"},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
c := caddy.NewTestController("dns", test.input)
|
||||
fs, err := parseForward(c)
|
||||
|
||||
if test.expectedErr != "" && err == nil {
|
||||
t.Errorf("Test %d: expected error but found %s for input %s", i, err, test.input)
|
||||
}
|
||||
if err != nil {
|
||||
if test.expectedErr == "" {
|
||||
t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.input, err)
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), test.expectedErr) {
|
||||
t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.input)
|
||||
}
|
||||
}
|
||||
if test.expectedErr == "" {
|
||||
f := fs[0]
|
||||
if !test.expectedSourceAddress.Equal(f.sourceAddress) {
|
||||
t.Errorf("Test %d: expected: %v, got: %v", i, test.expectedSourceAddress, f.sourceAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitZone(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
|
||||
@@ -97,12 +97,21 @@ func (t *Transport) Dial(proto string) (*persistConn, bool, error) {
|
||||
|
||||
reqTime := time.Now()
|
||||
timeout := t.dialTimeout()
|
||||
if proto == "tcp-tls" {
|
||||
conn, err := dns.DialTimeoutWithTLS("tcp", t.addr, t.tlsConfig, timeout)
|
||||
t.updateDialTimeout(time.Since(reqTime))
|
||||
return &persistConn{c: conn, created: time.Now()}, false, err
|
||||
dialer := &net.Dialer{Timeout: timeout}
|
||||
|
||||
if t.localAddress != nil {
|
||||
if proto == "udp" {
|
||||
dialer.LocalAddr = &net.UDPAddr{IP: t.localAddress}
|
||||
} else {
|
||||
dialer.LocalAddr = &net.TCPAddr{IP: t.localAddress}
|
||||
}
|
||||
}
|
||||
conn, err := dns.DialTimeout(proto, t.addr, timeout)
|
||||
|
||||
// pass nil tlsConfig to use system default
|
||||
client := dns.Client{Net: proto, Dialer: dialer, TLSConfig: t.tlsConfig}
|
||||
|
||||
conn, err := client.Dial(t.addr)
|
||||
|
||||
t.updateDialTimeout(time.Since(reqTime))
|
||||
return &persistConn{c: conn, created: time.Now()}, false, err
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package proxy
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -28,6 +29,8 @@ type HealthChecker interface {
|
||||
SetReadTimeout(time.Duration)
|
||||
GetWriteTimeout() time.Duration
|
||||
SetWriteTimeout(time.Duration)
|
||||
SetLocalAddress(net.IP)
|
||||
GetLocalAddress() net.IP
|
||||
}
|
||||
|
||||
// dnsHc is a health checker for a DNS endpoint (DNS, and DoT).
|
||||
@@ -37,6 +40,8 @@ type dnsHc struct {
|
||||
domain string
|
||||
|
||||
proxyName string
|
||||
|
||||
localAddress net.IP
|
||||
}
|
||||
|
||||
const defaultTimeout = 1 * time.Second
|
||||
@@ -47,8 +52,7 @@ func NewHealthChecker(proxyName, protocol string, recursionDesired bool, domain
|
||||
case transport.DNS, transport.TLS:
|
||||
c := new(dns.Client)
|
||||
c.Net = "udp"
|
||||
c.ReadTimeout = defaultTimeout
|
||||
c.WriteTimeout = defaultTimeout
|
||||
setDefaultTimeout(c)
|
||||
|
||||
return &dnsHc{
|
||||
c: c,
|
||||
@@ -78,6 +82,8 @@ func NewHealthChecker(proxyName, protocol string, recursionDesired bool, domain
|
||||
func (h *dnsHc) SetTLSConfig(cfg *tls.Config) {
|
||||
h.c.Net = "tcp-tls"
|
||||
h.c.TLSConfig = cfg
|
||||
// update the dialer accordingly with the protocol changed
|
||||
h.setDialer()
|
||||
}
|
||||
|
||||
func (h *dnsHc) GetTLSConfig() *tls.Config {
|
||||
@@ -100,6 +106,8 @@ func (h *dnsHc) GetDomain() string {
|
||||
|
||||
func (h *dnsHc) SetTCPTransport() {
|
||||
h.c.Net = "tcp"
|
||||
// update the dialer accordingly with the protocol changed
|
||||
h.setDialer()
|
||||
}
|
||||
|
||||
func (h *dnsHc) GetReadTimeout() time.Duration {
|
||||
@@ -151,12 +159,49 @@ func (h *dnsHc) send(addr string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// SetLocalAddress sets the local address in transport.
|
||||
func (h *dnsHc) SetLocalAddress(localAddr net.IP) {
|
||||
h.localAddress = localAddr
|
||||
h.setDialer()
|
||||
}
|
||||
|
||||
// GetLocalAddress returns the local address in transport.
|
||||
func (h *dnsHc) GetLocalAddress() net.IP {
|
||||
return h.localAddress
|
||||
}
|
||||
|
||||
// setDialer sets the local address in the underlying dialer
|
||||
func (h *dnsHc) setDialer() {
|
||||
if h.localAddress == nil {
|
||||
if h.c.Dialer != nil {
|
||||
h.c.Dialer.LocalAddr = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
if h.c.Dialer == nil {
|
||||
h.c.Dialer = new(net.Dialer)
|
||||
setDefaultTimeout(h.c)
|
||||
}
|
||||
if h.c.Net == "udp" {
|
||||
h.c.Dialer.LocalAddr = &net.UDPAddr{IP: h.localAddress}
|
||||
} else {
|
||||
h.c.Dialer.LocalAddr = &net.TCPAddr{IP: h.localAddress}
|
||||
}
|
||||
}
|
||||
|
||||
// setDefaultTimeout sets the default read and write timeout values for the DNS client to 1 second.
|
||||
func setDefaultTimeout(c *dns.Client) {
|
||||
c.ReadTimeout = 1 * time.Second
|
||||
c.WriteTimeout = 1 * time.Second
|
||||
}
|
||||
|
||||
// dohHc is a health checker for a DNS-over-HTTPS (DoH) endpoint.
|
||||
type dohHc struct {
|
||||
client *http.Client
|
||||
recursionDesired bool
|
||||
domain string
|
||||
proxyName string
|
||||
localAddress net.IP
|
||||
}
|
||||
|
||||
func (h *dohHc) Check(p *Proxy) error {
|
||||
@@ -244,3 +289,18 @@ func (h *dohHc) GetWriteTimeout() time.Duration {
|
||||
func (h *dohHc) SetWriteTimeout(t time.Duration) {
|
||||
h.client.Timeout = t
|
||||
}
|
||||
|
||||
func (h *dohHc) SetLocalAddress(localAddr net.IP) {
|
||||
h.localAddress = localAddr
|
||||
httpTransport := h.client.Transport.(*http.Transport)
|
||||
if localAddr == nil {
|
||||
httpTransport.DialContext = nil
|
||||
return
|
||||
}
|
||||
dialer := &net.Dialer{LocalAddr: &net.TCPAddr{IP: localAddr}}
|
||||
httpTransport.DialContext = dialer.DialContext
|
||||
}
|
||||
|
||||
func (h *dohHc) GetLocalAddress() net.IP {
|
||||
return h.localAddress
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
@@ -200,3 +202,44 @@ func TestHealthDomain(t *testing.T) {
|
||||
t.Errorf("Expected number of health checks with Domain==%s to be %d, got %d", hcDomain, 1, i1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthLocalAddress(t *testing.T) {
|
||||
hc := NewHealthChecker("TestHealthLocalAddress", transport.DNS, true, ".")
|
||||
hc.SetReadTimeout(10 * time.Millisecond)
|
||||
hc.SetWriteTimeout(10 * time.Millisecond)
|
||||
currLocalAddress := hc.GetLocalAddress()
|
||||
if currLocalAddress != nil {
|
||||
t.Errorf("Expected local address to be nil, got %s", currLocalAddress.String())
|
||||
}
|
||||
|
||||
hc.SetLocalAddress(net.ParseIP("127.0.0.1"))
|
||||
dnsClient := hc.(*dnsHc).c
|
||||
if dnsClient.Dialer.LocalAddr.String() != "127.0.0.1:0" {
|
||||
t.Errorf("Expected local address to be 127.0.0.1:0, got %s", dnsClient.Dialer.LocalAddr.String())
|
||||
}
|
||||
|
||||
// check type of underlying transport
|
||||
_, ok := dnsClient.Dialer.LocalAddr.(*net.UDPAddr)
|
||||
if !ok {
|
||||
t.Error("Expected local address to be udp")
|
||||
}
|
||||
|
||||
// set TCP transport
|
||||
hc.SetTCPTransport()
|
||||
|
||||
// check update of underlying transport
|
||||
_, ok = dnsClient.Dialer.LocalAddr.(*net.TCPAddr)
|
||||
if !ok {
|
||||
t.Error("Expected local address to be tcp")
|
||||
}
|
||||
|
||||
tlsConfig := new(tls.Config)
|
||||
// set TLS transport
|
||||
hc.SetTLSConfig(tlsConfig)
|
||||
|
||||
// check update of underlying transport
|
||||
_, ok = dnsClient.Dialer.LocalAddr.(*net.TCPAddr)
|
||||
if !ok {
|
||||
t.Error("Expected local address to be tcp")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package proxy
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"sync"
|
||||
@@ -28,6 +29,7 @@ type Transport struct {
|
||||
tlsConfig *tls.Config
|
||||
httpClient *http.Client
|
||||
proxyName string
|
||||
localAddress net.IP
|
||||
|
||||
mu sync.Mutex
|
||||
stop chan struct{}
|
||||
@@ -171,6 +173,11 @@ func (t *Transport) SetTLSConfig(cfg *tls.Config) { t.tlsConfig = cfg }
|
||||
// GetTLSConfig returns the TLS config in transport.
|
||||
func (t *Transport) GetTLSConfig() *tls.Config { return t.tlsConfig }
|
||||
|
||||
// SetLocalAddress sets the local address in transport.
|
||||
func (t *Transport) SetLocalAddress(addr net.IP) {
|
||||
t.localAddress = addr
|
||||
}
|
||||
|
||||
const (
|
||||
defaultExpire = 10 * time.Second
|
||||
minDialTimeout = 1 * time.Second
|
||||
|
||||
@@ -2,6 +2,7 @@ package proxy
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
@@ -135,6 +136,20 @@ func (p *Proxy) incrementFails() {
|
||||
atomic.AddUint32(&p.fails, 1)
|
||||
}
|
||||
|
||||
// SetLocalAddress sets the local address for the proxy, used as the source address for outbound connections.
|
||||
func (p *Proxy) SetLocalAddress(addr net.IP) {
|
||||
p.transport.SetLocalAddress(addr)
|
||||
if p.transport.httpClient != nil {
|
||||
httpTransport := p.transport.httpClient.Transport.(*http.Transport)
|
||||
if addr == nil {
|
||||
httpTransport.DialContext = nil
|
||||
return
|
||||
}
|
||||
dialer := &net.Dialer{LocalAddr: &net.TCPAddr{IP: addr}}
|
||||
httpTransport.DialContext = dialer.DialContext
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
maxTimeout = 2 * time.Second
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user