mirror of
https://github.com/coredns/coredns.git
synced 2026-10-25 03:35:44 -04:00
core/dnsserver: preserve UDP packet destination addresses (#8644)
* core/dnsserver: preserve UDP packet destination addresses Signed-off-by: houyuwushang <180804215+houyuwushang@users.noreply.github.com> * test: modernize reverse UDP reply traversal Signed-off-by: houyuwushang <180804215+houyuwushang@users.noreply.github.com> * test: account for Unix IPv4 source-control capabilities Signed-off-by: houyuwushang <180804215+houyuwushang@users.noreply.github.com> --------- Signed-off-by: houyuwushang <180804215+houyuwushang@users.noreply.github.com>
This commit is contained in:
25
.github/workflows/go.test.yml
vendored
25
.github/workflows/go.test.yml
vendored
@@ -69,6 +69,9 @@ jobs:
|
|||||||
- name: Test manual-registration mode
|
- name: Test manual-registration mode
|
||||||
run: go test -race -tags=coredns_manual_registration ./core/dnsserver ./coremain
|
run: go test -race -tags=coredns_manual_registration ./core/dnsserver ./coremain
|
||||||
|
|
||||||
|
- name: Test UDP destination views
|
||||||
|
run: go test -race ./test -run '^TestViewServerIP'
|
||||||
|
|
||||||
test-plugins:
|
test-plugins:
|
||||||
name: Test Plugins
|
name: Test Plugins
|
||||||
strategy:
|
strategy:
|
||||||
@@ -94,6 +97,28 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: ( cd plugin; go test -race ./... )
|
run: ( cd plugin; go test -race ./... )
|
||||||
|
|
||||||
|
test-udp-macos:
|
||||||
|
name: Test UDP macOS
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||||
|
with:
|
||||||
|
go-version-file: .go-version
|
||||||
|
|
||||||
|
- name: Add a second loopback address
|
||||||
|
run: sudo ifconfig lo0 alias 127.0.0.2
|
||||||
|
|
||||||
|
- name: Test UDP destination addresses
|
||||||
|
run: |
|
||||||
|
go test -race ./core/dnsserver ./plugin/pkg/proxyproto ./request
|
||||||
|
go test -race ./test -run '^TestViewServerIP'
|
||||||
|
|
||||||
test-e2e:
|
test-e2e:
|
||||||
name: Test e2e
|
name: Test e2e
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ func (s *Server) Serve(l net.Listener) error {
|
|||||||
// ServePacket starts the server with an existing packetconn. It blocks until the server stops.
|
// ServePacket starts the server with an existing packetconn. It blocks until the server stops.
|
||||||
// This implements caddy.UDPServer interface.
|
// This implements caddy.UDPServer interface.
|
||||||
func (s *Server) ServePacket(p net.PacketConn) error {
|
func (s *Server) ServePacket(p net.PacketConn) error {
|
||||||
|
p = withUDPDestination(p)
|
||||||
// Use a custom writer decorator if one was configured.
|
// Use a custom writer decorator if one was configured.
|
||||||
var dw dns.DecorateWriter
|
var dw dns.DecorateWriter
|
||||||
if s.udpDecorateWriterFunc != nil {
|
if s.udpDecorateWriterFunc != nil {
|
||||||
@@ -249,7 +250,7 @@ func (s *Server) ServePacket(p net.PacketConn) error {
|
|||||||
s.server[udp] = &dns.Server{PacketConn: p, Net: "udp", Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
s.server[udp] = &dns.Server{PacketConn: p, Net: "udp", Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
ctx := context.WithValue(context.Background(), Key{}, s)
|
ctx := context.WithValue(context.Background(), Key{}, s)
|
||||||
ctx = context.WithValue(ctx, LoopKey{}, 0)
|
ctx = context.WithValue(ctx, LoopKey{}, 0)
|
||||||
s.ServeDNS(ctx, w, r)
|
s.ServeDNS(ctx, withUDPLocalAddr(w), r)
|
||||||
}), TsigSecret: s.TsigSecret, MsgAcceptFunc: s.msgAcceptFunc(), DecorateWriter: dw}
|
}), TsigSecret: s.TsigSecret, MsgAcceptFunc: s.msgAcceptFunc(), DecorateWriter: dw}
|
||||||
s.m.Unlock()
|
s.m.Unlock()
|
||||||
|
|
||||||
|
|||||||
131
core/dnsserver/udp.go
Normal file
131
core/dnsserver/udp.go
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
package dnsserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
cproxyproto "github.com/coredns/coredns/plugin/pkg/proxyproto"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/pires/go-proxyproto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// withUDPDestination preserves the destination of each packet on wildcard UDP
|
||||||
|
// listeners. Keep the original listener in Caddy: ServePacket also receives
|
||||||
|
// sockets inherited during a reload and must install the adapter there.
|
||||||
|
func withUDPDestination(p net.PacketConn) net.PacketConn {
|
||||||
|
if proxy, ok := p.(*cproxyproto.PacketConn); ok {
|
||||||
|
wrapped := *proxy
|
||||||
|
wrapped.PacketConn = withUDPDestination(proxy.PacketConn)
|
||||||
|
if proxy.ConnPolicy != nil {
|
||||||
|
wrapped.ConnPolicy = func(options proxyproto.ConnPolicyOptions) (proxyproto.Policy, error) {
|
||||||
|
// Policy callbacks must keep seeing the native transport peer.
|
||||||
|
if packet, ok := options.Upstream.(*udpDestinationAddr); ok {
|
||||||
|
options.Upstream = packet.UDPAddr
|
||||||
|
}
|
||||||
|
return proxy.ConnPolicy(options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &wrapped
|
||||||
|
}
|
||||||
|
conn, ok := p.(*net.UDPConn)
|
||||||
|
if !ok {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
local := conn.LocalAddr().(*net.UDPAddr)
|
||||||
|
// Bound sockets already report their local address. In particular, avoid
|
||||||
|
// sending packet control messages on bound Darwin sockets (miekg/dns#724).
|
||||||
|
if !local.IP.IsUnspecified() || !enableUDPPacketInfo(conn) {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
return &udpDestinationConn{UDPConn: conn, local: *local}
|
||||||
|
}
|
||||||
|
|
||||||
|
type udpDestinationConn struct {
|
||||||
|
*net.UDPConn
|
||||||
|
local net.UDPAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
type udpPacketInfo struct {
|
||||||
|
dst net.IP
|
||||||
|
ifIndex int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep normal route selection for unicast replies. Scoped IPv6 addresses also
|
||||||
|
// need their interface; a global address must not force the incoming route.
|
||||||
|
func (p udpPacketInfo) replyInterface() int {
|
||||||
|
if p.dst.To4() == nil && p.dst.IsLinkLocalUnicast() {
|
||||||
|
return p.ifIndex
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// udpDestinationAddr travels through dns.Server's generic PacketConn path,
|
||||||
|
// which retains the ReadFrom address for both RemoteAddr and WriteTo. All
|
||||||
|
// packet information belongs to this request, not to the shared listener.
|
||||||
|
type udpDestinationAddr struct {
|
||||||
|
*net.UDPAddr
|
||||||
|
local net.UDPAddr
|
||||||
|
info udpPacketInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *udpDestinationAddr) LocalAddr() net.Addr { return &a.local }
|
||||||
|
|
||||||
|
func (c *udpDestinationConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||||
|
oob := make([]byte, udpOOBSize)
|
||||||
|
n, oobn, flags, peer, err := c.ReadMsgUDP(b, oob)
|
||||||
|
if err != nil {
|
||||||
|
return n, peer, err
|
||||||
|
}
|
||||||
|
info := parseUDPPacketInfo(oob[:oobn], flags)
|
||||||
|
if info.dst == nil {
|
||||||
|
return n, peer, nil
|
||||||
|
}
|
||||||
|
if ip4 := info.dst.To4(); ip4 != nil {
|
||||||
|
info.dst = ip4
|
||||||
|
}
|
||||||
|
local := net.UDPAddr{IP: info.dst, Port: c.local.Port}
|
||||||
|
if info.dst.To4() == nil && (info.dst.IsLinkLocalUnicast() || info.dst.IsLinkLocalMulticast() || info.dst.IsInterfaceLocalMulticast()) && info.ifIndex > 0 {
|
||||||
|
local.Zone = strconv.Itoa(info.ifIndex)
|
||||||
|
}
|
||||||
|
return n, &udpDestinationAddr{UDPAddr: peer, local: local, info: info}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *udpDestinationConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
||||||
|
if packet, ok := addr.(*udpDestinationAddr); ok {
|
||||||
|
n, _, err := c.WriteMsgUDP(b, udpSourceControl(packet.info), packet.UDPAddr)
|
||||||
|
if err == nil {
|
||||||
|
// Datagram writes are atomic. Windows can report zero bytes for a
|
||||||
|
// successful synchronous WSASendMsg on supported Go versions.
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
return c.UDPConn.WriteTo(b, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
type udpResponseWriter struct {
|
||||||
|
dns.ResponseWriter
|
||||||
|
local, remote net.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *udpResponseWriter) LocalAddr() net.Addr { return w.local }
|
||||||
|
func (w *udpResponseWriter) RemoteAddr() net.Addr { return w.remote }
|
||||||
|
|
||||||
|
func withUDPLocalAddr(w dns.ResponseWriter) dns.ResponseWriter {
|
||||||
|
remote := w.RemoteAddr()
|
||||||
|
packet, ok := remote.(interface{ LocalAddr() net.Addr })
|
||||||
|
if !ok {
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
local := packet.LocalAddr()
|
||||||
|
if local == nil {
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
// Preserve the native peer type on plain UDP and the existing proxyproto
|
||||||
|
// address (including its client address and TLVs) on PROXY listeners.
|
||||||
|
if addr, ok := remote.(*udpDestinationAddr); ok {
|
||||||
|
remote = addr.UDPAddr
|
||||||
|
}
|
||||||
|
return &udpResponseWriter{ResponseWriter: w, local: local, remote: remote}
|
||||||
|
}
|
||||||
11
core/dnsserver/udp_other.go
Normal file
11
core/dnsserver/udp_other.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows
|
||||||
|
|
||||||
|
package dnsserver
|
||||||
|
|
||||||
|
import "net"
|
||||||
|
|
||||||
|
const udpOOBSize = 0
|
||||||
|
|
||||||
|
func enableUDPPacketInfo(*net.UDPConn) bool { return false }
|
||||||
|
func parseUDPPacketInfo([]byte, int) udpPacketInfo { return udpPacketInfo{} }
|
||||||
|
func udpSourceControl(udpPacketInfo) []byte { return nil }
|
||||||
371
core/dnsserver/udp_test.go
Normal file
371
core/dnsserver/udp_test.go
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows
|
||||||
|
|
||||||
|
package dnsserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"runtime"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
cproxyproto "github.com/coredns/coredns/plugin/pkg/proxyproto"
|
||||||
|
"github.com/coredns/coredns/request"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/pires/go-proxyproto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func udpTestConn(t *testing.T, network, address string) *net.UDPConn {
|
||||||
|
t.Helper()
|
||||||
|
addr, err := net.ResolveUDPAddr(network, address)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
conn, err := net.ListenUDP(network, addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { conn.Close() })
|
||||||
|
if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two requests from the same peer must keep independent destination addresses,
|
||||||
|
// even when their replies are sent in the opposite order.
|
||||||
|
func TestUDPDestinationReadWrite(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name, network, bind string
|
||||||
|
destinations []string
|
||||||
|
}{
|
||||||
|
{"ipv4", "udp4", "0.0.0.0:0", []string{"127.0.0.1", "127.0.0.2"}},
|
||||||
|
{"ipv6", "udp6", "[::]:0", []string{"::1", "::1"}},
|
||||||
|
{"dual_stack", "udp", "[::]:0", []string{"127.0.0.1", "::1"}},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if tc.name == "dual_stack" && (runtime.GOOS == "openbsd" || runtime.GOOS == "dragonfly") {
|
||||||
|
t.Skip("the kernel requires IPv6-only sockets")
|
||||||
|
}
|
||||||
|
if tc.network != "udp4" {
|
||||||
|
probe, err := net.ListenPacket("udp6", "[::1]:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("IPv6 loopback is unavailable: %v", err)
|
||||||
|
}
|
||||||
|
probe.Close()
|
||||||
|
}
|
||||||
|
if tc.name == "ipv4" {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
// Winsock cannot select an unassigned 127.0.0.2 as the
|
||||||
|
// source. Use a second configured local address instead.
|
||||||
|
tc.destinations[1] = secondLocalIPv4(t)
|
||||||
|
case "aix", "dragonfly", "freebsd", "netbsd", "openbsd":
|
||||||
|
// x/net can receive IPv4 destinations but cannot marshal
|
||||||
|
// IPv4 source control on these systems. Preserve their
|
||||||
|
// existing routing behavior with one destination here.
|
||||||
|
tc.destinations[1] = tc.destinations[0]
|
||||||
|
default:
|
||||||
|
probe, err := net.ListenPacket("udp4", "127.0.0.2:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("second loopback address is unavailable: %v", err)
|
||||||
|
}
|
||||||
|
probe.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
raw := udpTestConn(t, tc.network, tc.bind)
|
||||||
|
conn := withUDPDestination(raw)
|
||||||
|
if conn == raw {
|
||||||
|
t.Fatal("wildcard listener did not enable packet information")
|
||||||
|
}
|
||||||
|
port := raw.LocalAddr().(*net.UDPAddr).Port
|
||||||
|
peer := udpTestConn(t, tc.network, tc.bind)
|
||||||
|
var addresses []net.Addr
|
||||||
|
for i, dst := range tc.destinations {
|
||||||
|
payload := []byte{byte(i)}
|
||||||
|
if _, err := peer.WriteToUDP(payload, &net.UDPAddr{IP: net.ParseIP(dst), Port: port}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
buf := make([]byte, 16)
|
||||||
|
n, addr, err := conn.ReadFrom(buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(buf[:n], payload) {
|
||||||
|
t.Fatalf("received %v, want %v", buf[:n], payload)
|
||||||
|
}
|
||||||
|
addresses = append(addresses, addr)
|
||||||
|
}
|
||||||
|
for i, addr := range slices.Backward(addresses) {
|
||||||
|
packet, ok := addr.(interface{ LocalAddr() net.Addr })
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("missing destination on %T", addr)
|
||||||
|
}
|
||||||
|
want := net.JoinHostPort(tc.destinations[i], strconv.Itoa(port))
|
||||||
|
if got := packet.LocalAddr().String(); got != want {
|
||||||
|
t.Errorf("destination = %s, want %s", got, want)
|
||||||
|
}
|
||||||
|
if n, err := conn.WriteTo([]byte{byte(i)}, addr); err != nil || n != 1 {
|
||||||
|
t.Fatalf("WriteTo = %d, %v, want 1 byte", n, err)
|
||||||
|
}
|
||||||
|
buf := make([]byte, 16)
|
||||||
|
n, source, err := peer.ReadFromUDP(buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if n != 1 || buf[0] != byte(i) || source.String() != want {
|
||||||
|
t.Fatalf("reply = %v from %s, want [%d] from %s", buf[:n], source, i, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func secondLocalIPv4(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
interfaces, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, iface := range interfaces {
|
||||||
|
if iface.Flags&net.FlagUp == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addrs, err := iface.Addrs()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, addr := range addrs {
|
||||||
|
ip, _, err := net.ParseCIDR(addr.String())
|
||||||
|
if err == nil && ip.To4() != nil && ip.IsPrivate() && !ip.IsLoopback() {
|
||||||
|
return ip.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Skip("a second configured local IPv4 address is unavailable")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUDPDestinationBoundSocket(t *testing.T) {
|
||||||
|
conn := udpTestConn(t, "udp4", "127.0.0.1:0")
|
||||||
|
if withUDPDestination(conn) != conn {
|
||||||
|
t.Fatal("explicitly bound listener should keep its existing UDP path")
|
||||||
|
}
|
||||||
|
addr := conn.LocalAddr().String()
|
||||||
|
peer := udpTestConn(t, "udp4", "127.0.0.1:0")
|
||||||
|
if _, err := peer.WriteToUDP([]byte("bound"), conn.LocalAddr().(*net.UDPAddr)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
buf := make([]byte, 16)
|
||||||
|
n, from, err := conn.ReadFrom(buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := conn.WriteTo(buf[:n], from); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, _, err := peer.ReadFrom(buf); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := conn.LocalAddr().String(); got != addr {
|
||||||
|
t.Fatalf("socket binding changed after reply: %s -> %s", addr, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUDPDestinationDeadlineAndClose(t *testing.T) {
|
||||||
|
raw := udpTestConn(t, "udp4", "0.0.0.0:0")
|
||||||
|
conn := withUDPDestination(raw)
|
||||||
|
if err := conn.SetReadDeadline(time.Now().Add(-time.Second)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, _, err := conn.ReadFrom(make([]byte, 16))
|
||||||
|
var netErr net.Error
|
||||||
|
if !errors.As(err, &netErr) || !netErr.Timeout() {
|
||||||
|
t.Fatalf("expired deadline: %v", err)
|
||||||
|
}
|
||||||
|
if err := conn.SetReadDeadline(time.Time{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
_, _, err := conn.ReadFrom(make([]byte, 16))
|
||||||
|
done <- err
|
||||||
|
}()
|
||||||
|
conn.Close()
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
if !errors.Is(err, net.ErrClosed) {
|
||||||
|
t.Fatalf("closed socket: %v", err)
|
||||||
|
}
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("closing the socket did not unblock ReadFrom")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUDPDestinationMissingPacketInfo(t *testing.T) {
|
||||||
|
raw := udpTestConn(t, "udp4", "0.0.0.0:0")
|
||||||
|
// Deliberately leave packet-info socket options disabled.
|
||||||
|
conn := &udpDestinationConn{UDPConn: raw, local: *raw.LocalAddr().(*net.UDPAddr)}
|
||||||
|
peer := udpTestConn(t, "udp4", "127.0.0.1:0")
|
||||||
|
dst := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: conn.local.Port}
|
||||||
|
if _, err := peer.WriteToUDP([]byte("fallback"), dst); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
buf := make([]byte, 16)
|
||||||
|
n, addr, err := conn.ReadFrom(buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, ok := addr.(*net.UDPAddr); !ok {
|
||||||
|
t.Fatalf("missing metadata changed the peer address: %T", addr)
|
||||||
|
}
|
||||||
|
if conn.LocalAddr().String() != raw.LocalAddr().String() {
|
||||||
|
t.Fatal("missing metadata changed the listener address")
|
||||||
|
}
|
||||||
|
if count, err := conn.WriteTo(buf[:n], addr); err != nil || count != n {
|
||||||
|
t.Fatalf("fallback reply: %d, %v", count, err)
|
||||||
|
}
|
||||||
|
if _, _, err := peer.ReadFrom(buf); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type udpAddressPlugin struct{}
|
||||||
|
|
||||||
|
func (udpAddressPlugin) Name() string { return "udp-address" }
|
||||||
|
func (udpAddressPlugin) ServeDNS(_ context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||||
|
state := request.Request{W: w, Req: r}
|
||||||
|
rawTLVs := ""
|
||||||
|
if peer, ok := w.RemoteAddr().(*cproxyproto.Addr); ok {
|
||||||
|
rawTLVs = hex.EncodeToString(peer.RawTLVs())
|
||||||
|
}
|
||||||
|
m := new(dns.Msg).SetReply(r)
|
||||||
|
m.Answer = []dns.RR{&dns.TXT{
|
||||||
|
Hdr: dns.RR_Header{Name: r.Question[0].Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET},
|
||||||
|
Txt: []string{state.LocalIP(), state.LocalPort(), state.IP(), state.Proto(), strconv.Itoa(state.Family()), rawTLVs},
|
||||||
|
}}
|
||||||
|
return dns.RcodeSuccess, w.WriteMsg(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUDPDestinationServer(t *testing.T) {
|
||||||
|
for _, mode := range []string{"plain", "proxy", "proxy_local", "proxy_skip"} {
|
||||||
|
t.Run(mode, func(t *testing.T) {
|
||||||
|
raw := udpTestConn(t, "udp4", "0.0.0.0:0")
|
||||||
|
port := raw.LocalAddr().(*net.UDPAddr).Port
|
||||||
|
var pc net.PacketConn = raw
|
||||||
|
if mode != "plain" {
|
||||||
|
pc = &cproxyproto.PacketConn{PacketConn: raw, CaptureTLVs: true, UDPSessionTrackingTTL: time.Minute,
|
||||||
|
ConnPolicy: func(options proxyproto.ConnPolicyOptions) (proxyproto.Policy, error) {
|
||||||
|
if _, ok := options.Upstream.(*net.UDPAddr); !ok {
|
||||||
|
t.Errorf("policy peer has type %T, want *net.UDPAddr", options.Upstream)
|
||||||
|
}
|
||||||
|
if options.Downstream.String() != raw.LocalAddr().String() {
|
||||||
|
t.Errorf("policy listener = %s, want %s", options.Downstream, raw.LocalAddr())
|
||||||
|
}
|
||||||
|
if mode == "proxy_skip" {
|
||||||
|
return proxyproto.SKIP, nil
|
||||||
|
}
|
||||||
|
return proxyproto.USE, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ready := make(chan struct{})
|
||||||
|
cfg := testConfig("dns", udpAddressPlugin{})
|
||||||
|
cfg.UDPDecorateWriterFunc = func(*Server) dns.DecorateWriter { close(ready); return nil }
|
||||||
|
srv, err := NewServer("0.0.0.0:0", []*Config{cfg})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() { done <- srv.ServePacket(pc) }()
|
||||||
|
select {
|
||||||
|
case <-ready:
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("UDP adapter did not start")
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := srv.Stop(); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Error("UDP server did not stop")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
conn, err := net.Dial("udp4", net.JoinHostPort("127.0.0.1", strconv.Itoa(port)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||||
|
wantRemote, wantTLVs := "127.0.0.1", ""
|
||||||
|
if mode == "proxy" {
|
||||||
|
wantRemote, wantTLVs = "192.0.2.10", "ea00020102"
|
||||||
|
}
|
||||||
|
for i := range 2 {
|
||||||
|
m := new(dns.Msg).SetQuestion("example.com.", dns.TypeTXT)
|
||||||
|
packet, err := m.Pack()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if i == 0 && (mode == "proxy" || mode == "proxy_local") {
|
||||||
|
header := &proxyproto.Header{Version: 2, Command: proxyproto.PROXY, TransportProtocol: proxyproto.UDPv4,
|
||||||
|
SourceAddr: &net.UDPAddr{IP: net.ParseIP("192.0.2.10"), Port: 1234},
|
||||||
|
DestinationAddr: &net.UDPAddr{IP: net.ParseIP("198.51.100.53"), Port: 53}}
|
||||||
|
if mode == "proxy_local" {
|
||||||
|
header.Command = proxyproto.LOCAL
|
||||||
|
}
|
||||||
|
if err := header.SetTLVs([]proxyproto.TLV{{Type: 0xEA, Value: []byte{1, 2}}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
prefix, err := header.Format()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
packet = append(prefix, packet...)
|
||||||
|
}
|
||||||
|
if _, err := conn.Write(packet); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
answer, err := (&dns.Conn{Conn: conn}).ReadMsg()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if answer.Rcode != dns.RcodeSuccess || len(answer.Answer) != 1 {
|
||||||
|
t.Fatalf("unexpected response: %v", answer)
|
||||||
|
}
|
||||||
|
want := []string{"127.0.0.1", strconv.Itoa(port), wantRemote, "udp", "1", wantTLVs}
|
||||||
|
got := answer.Answer[0].(*dns.TXT).Txt
|
||||||
|
for j := range want {
|
||||||
|
if got[j] != want[j] {
|
||||||
|
t.Errorf("field %d = %q, want %q", j, got[j], want[j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzUDPPacketInfo(f *testing.F) {
|
||||||
|
f.Add([]byte{})
|
||||||
|
f.Add([]byte{0xff, 0xff, 0xff, 0xff})
|
||||||
|
f.Add(udpSourceControl(udpPacketInfo{dst: net.ParseIP("127.0.0.1"), ifIndex: 1}))
|
||||||
|
f.Add(udpSourceControl(udpPacketInfo{dst: net.ParseIP("::1"), ifIndex: 1}))
|
||||||
|
f.Fuzz(func(t *testing.T, data []byte) {
|
||||||
|
info := parseUDPPacketInfo(data, 0)
|
||||||
|
if info.dst != nil && info.dst.To16() == nil {
|
||||||
|
t.Fatalf("invalid destination: %v", info.dst)
|
||||||
|
}
|
||||||
|
udpSourceControl(info)
|
||||||
|
})
|
||||||
|
}
|
||||||
45
core/dnsserver/udp_unix.go
Normal file
45
core/dnsserver/udp_unix.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||||
|
|
||||||
|
package dnsserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"golang.org/x/net/ipv4"
|
||||||
|
"golang.org/x/net/ipv6"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
var udpOOBSize = len(ipv4.NewControlMessage(ipv4.FlagDst|ipv4.FlagInterface)) +
|
||||||
|
len(ipv6.NewControlMessage(ipv6.FlagDst|ipv6.FlagInterface))
|
||||||
|
|
||||||
|
func enableUDPPacketInfo(conn *net.UDPConn) bool {
|
||||||
|
err4 := ipv4.NewPacketConn(conn).SetControlMessage(ipv4.FlagDst|ipv4.FlagInterface, true)
|
||||||
|
err6 := ipv6.NewPacketConn(conn).SetControlMessage(ipv6.FlagDst|ipv6.FlagInterface, true)
|
||||||
|
return err4 == nil || err6 == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUDPPacketInfo(oob []byte, flags int) udpPacketInfo {
|
||||||
|
if flags&unix.MSG_CTRUNC != 0 {
|
||||||
|
return udpPacketInfo{}
|
||||||
|
}
|
||||||
|
cm6 := new(ipv6.ControlMessage)
|
||||||
|
if cm6.Parse(oob) == nil && cm6.Dst != nil {
|
||||||
|
return udpPacketInfo{dst: cm6.Dst, ifIndex: cm6.IfIndex}
|
||||||
|
}
|
||||||
|
cm4 := new(ipv4.ControlMessage)
|
||||||
|
if cm4.Parse(oob) == nil && cm4.Dst != nil {
|
||||||
|
return udpPacketInfo{dst: cm4.Dst, ifIndex: cm4.IfIndex}
|
||||||
|
}
|
||||||
|
return udpPacketInfo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func udpSourceControl(info udpPacketInfo) []byte {
|
||||||
|
if info.dst == nil || info.dst.IsUnspecified() || info.dst.IsMulticast() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if ip4 := info.dst.To4(); ip4 != nil {
|
||||||
|
return (&ipv4.ControlMessage{Src: ip4, IfIndex: info.replyInterface()}).Marshal()
|
||||||
|
}
|
||||||
|
return (&ipv6.ControlMessage{Src: info.dst, IfIndex: info.replyInterface()}).Marshal()
|
||||||
|
}
|
||||||
107
core/dnsserver/udp_windows.go
Normal file
107
core/dnsserver/udp_windows.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package dnsserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"math"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WSACMSGHDR contains a native-size length followed by two 32-bit integers.
|
||||||
|
// Each control message is aligned to SIZE_T; its length excludes tail padding.
|
||||||
|
const udpCmsgWord = strconv.IntSize / 8
|
||||||
|
const udpCmsgHeader = udpCmsgWord + 8
|
||||||
|
|
||||||
|
var udpOOBSize = udpCmsgAlign(udpCmsgHeader+8) + udpCmsgAlign(udpCmsgHeader+20)
|
||||||
|
|
||||||
|
func udpCmsgAlign(n int) int { return (n + udpCmsgWord - 1) & ^(udpCmsgWord - 1) }
|
||||||
|
|
||||||
|
func enableUDPPacketInfo(conn *net.UDPConn) bool {
|
||||||
|
raw, err := conn.SyscallConn()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var err4, err6 error
|
||||||
|
err = raw.Control(func(fd uintptr) {
|
||||||
|
// Dual-stack sockets need both options to receive both families.
|
||||||
|
err4 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, windows.IP_PKTINFO, 1)
|
||||||
|
err6 = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, windows.IPV6_PKTINFO, 1)
|
||||||
|
})
|
||||||
|
return err == nil && (err4 == nil || err6 == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUDPPacketInfo(oob []byte, flags int) udpPacketInfo {
|
||||||
|
if flags&windows.MSG_CTRUNC != 0 {
|
||||||
|
return udpPacketInfo{}
|
||||||
|
}
|
||||||
|
for len(oob) >= udpCmsgHeader {
|
||||||
|
var length uint64
|
||||||
|
if udpCmsgWord == 8 {
|
||||||
|
length = binary.NativeEndian.Uint64(oob)
|
||||||
|
} else {
|
||||||
|
length = uint64(binary.NativeEndian.Uint32(oob))
|
||||||
|
}
|
||||||
|
if length < udpCmsgHeader || length > uint64(len(oob)) || length > math.MaxInt32 {
|
||||||
|
return udpPacketInfo{}
|
||||||
|
}
|
||||||
|
level := binary.NativeEndian.Uint32(oob[udpCmsgWord:])
|
||||||
|
kind := binary.NativeEndian.Uint32(oob[udpCmsgWord+4:])
|
||||||
|
data := oob[udpCmsgHeader:length]
|
||||||
|
ipLen := 0
|
||||||
|
switch {
|
||||||
|
case level == windows.IPPROTO_IP && kind == windows.IP_PKTINFO:
|
||||||
|
ipLen = net.IPv4len
|
||||||
|
case level == windows.IPPROTO_IPV6 && kind == windows.IPV6_PKTINFO:
|
||||||
|
ipLen = net.IPv6len
|
||||||
|
}
|
||||||
|
if ipLen != 0 {
|
||||||
|
if len(data) < ipLen+4 {
|
||||||
|
return udpPacketInfo{}
|
||||||
|
}
|
||||||
|
ifIndex := binary.NativeEndian.Uint32(data[ipLen:])
|
||||||
|
if ifIndex > math.MaxInt32 {
|
||||||
|
return udpPacketInfo{}
|
||||||
|
}
|
||||||
|
return udpPacketInfo{dst: net.IP(data[:ipLen]), ifIndex: int(ifIndex)}
|
||||||
|
}
|
||||||
|
next := udpCmsgAlign(int(length))
|
||||||
|
if next < int(length) || next > len(oob) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
oob = oob[next:]
|
||||||
|
}
|
||||||
|
return udpPacketInfo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func udpSourceControl(info udpPacketInfo) []byte {
|
||||||
|
if info.dst == nil || info.dst.IsUnspecified() || info.dst.IsMulticast() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ifIndex := info.replyInterface()
|
||||||
|
if ifIndex < 0 || ifIndex > math.MaxInt32 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ip := info.dst.To4()
|
||||||
|
level, kind := uint32(windows.IPPROTO_IP), uint32(windows.IP_PKTINFO)
|
||||||
|
if ip == nil {
|
||||||
|
ip = info.dst.To16()
|
||||||
|
level, kind = windows.IPPROTO_IPV6, windows.IPV6_PKTINFO
|
||||||
|
}
|
||||||
|
if ip == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
length := udpCmsgHeader + len(ip) + 4
|
||||||
|
oob := make([]byte, udpCmsgAlign(length))
|
||||||
|
if udpCmsgWord == 8 {
|
||||||
|
binary.NativeEndian.PutUint64(oob, uint64(length))
|
||||||
|
} else {
|
||||||
|
binary.NativeEndian.PutUint32(oob, uint32(length)) //nolint:gosec // Header plus a 4- or 16-byte IP and a 4-byte interface.
|
||||||
|
}
|
||||||
|
binary.NativeEndian.PutUint32(oob[udpCmsgWord:], level)
|
||||||
|
binary.NativeEndian.PutUint32(oob[udpCmsgWord+4:], kind)
|
||||||
|
copy(oob[udpCmsgHeader:], ip)
|
||||||
|
binary.NativeEndian.PutUint32(oob[udpCmsgHeader+len(ip):], uint32(ifIndex))
|
||||||
|
return oob
|
||||||
|
}
|
||||||
74
core/dnsserver/udp_windows_test.go
Normal file
74
core/dnsserver/udp_windows_test.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package dnsserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUDPPacketInfoWindows(t *testing.T) {
|
||||||
|
// Native WSACMSGHDR followed by IN_PKTINFO/IN6_PKTINFO. These fixtures
|
||||||
|
// include the receiving interface, unlike unscoped send control messages.
|
||||||
|
ipv4 := "180000000000000000000000130000007f00000107000000"
|
||||||
|
ipv6 := "24000000000000002900000013000000fe8000000000000000000000000000010700000000000000"
|
||||||
|
if udpCmsgWord == 4 {
|
||||||
|
ipv4 = "1400000000000000130000007f00000107000000"
|
||||||
|
ipv6 = "200000002900000013000000fe80000000000000000000000000000107000000"
|
||||||
|
}
|
||||||
|
for _, tc := range []struct{ name, encoded, dst string }{
|
||||||
|
{"ipv4", ipv4, "127.0.0.1"},
|
||||||
|
{"ipv6", ipv6, "fe80::1"},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
control, err := hex.DecodeString(tc.encoded)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
info := parseUDPPacketInfo(control, 0)
|
||||||
|
if !info.dst.Equal(net.ParseIP(tc.dst)) || info.ifIndex != 7 {
|
||||||
|
t.Fatalf("packet info = %+v, want %s on interface 7", info, tc.dst)
|
||||||
|
}
|
||||||
|
if got := parseUDPPacketInfo(control, windows.MSG_CTRUNC); got.dst != nil {
|
||||||
|
t.Fatal("truncated control data must not supply a destination")
|
||||||
|
}
|
||||||
|
for n := range udpCmsgHeader + len(info.dst) + 4 {
|
||||||
|
if got := parseUDPPacketInfo(control[:n], 0); got.dst != nil {
|
||||||
|
t.Errorf("accepted truncated control data of length %d", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tc.name == "ipv4" {
|
||||||
|
// Global/loopback replies retain route selection; only scoped
|
||||||
|
// IPv6 replies carry an outgoing interface constraint.
|
||||||
|
binary.NativeEndian.PutUint32(control[udpCmsgHeader+4:], 0)
|
||||||
|
}
|
||||||
|
if got := udpSourceControl(info); !bytes.Equal(got, control) {
|
||||||
|
t.Errorf("source control = %x, want %x", got, control)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUDPPacketInfoWindowsMalformed(t *testing.T) {
|
||||||
|
valid := udpSourceControl(udpPacketInfo{dst: net.ParseIP("127.0.0.1")})
|
||||||
|
for _, length := range []uint64{0, udpCmsgHeader - 1, udpCmsgHeader, udpCmsgHeader + 3, uint64(len(valid) + 1), ^uint64(0)} {
|
||||||
|
control := bytes.Clone(valid)
|
||||||
|
if udpCmsgWord == 8 {
|
||||||
|
binary.NativeEndian.PutUint64(control, length)
|
||||||
|
} else {
|
||||||
|
binary.NativeEndian.PutUint32(control, uint32(length))
|
||||||
|
}
|
||||||
|
if got := parseUDPPacketInfo(control, 0); got.dst != nil {
|
||||||
|
t.Errorf("accepted control length %d", length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unknown := bytes.Clone(valid)
|
||||||
|
binary.NativeEndian.PutUint32(unknown[udpCmsgWord+4:], 0xffff)
|
||||||
|
control := append(unknown, valid...)
|
||||||
|
if got := parseUDPPacketInfo(control, 0); !got.dst.Equal(net.IPv4(127, 0, 0, 1)) {
|
||||||
|
t.Fatalf("did not find packet info after an unknown message: %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -179,7 +179,7 @@ functions defined below.
|
|||||||
.IP \(bu 4
|
.IP \(bu 4
|
||||||
\fB\fCproto() string\fR: protocol used (tcp or udp)
|
\fB\fCproto() string\fR: protocol used (tcp or udp)
|
||||||
.IP \(bu 4
|
.IP \(bu 4
|
||||||
\fB\fCserver_ip() string\fR: server's IP address; for IPv6 addresses these are enclosed in brackets: \fB\fC[::1]\fR
|
\fB\fCserver_ip() string\fR: server's destination IP address; IPv6 addresses have no brackets: \fB\fC::1\fR
|
||||||
.IP \(bu 4
|
.IP \(bu 4
|
||||||
\fB\fCserver_port() string\fR : server's port
|
\fB\fCserver_port() string\fR : server's port
|
||||||
.IP \(bu 4
|
.IP \(bu 4
|
||||||
@@ -188,6 +188,13 @@ functions defined below.
|
|||||||
\fB\fCtype() string\fR: type of the request (A, AAAA, TXT, ...)
|
\fB\fCtype() string\fR: type of the request (A, AAAA, TXT, ...)
|
||||||
|
|
||||||
|
|
||||||
|
.PP
|
||||||
|
For wildcard UDP listeners, \fB\fCserver_ip()\fR uses the destination address of the individual packet
|
||||||
|
when the operating system supplies packet information. If that information is unavailable, including
|
||||||
|
with custom packet listeners that do not expose it, the listening address is used instead.
|
||||||
|
With \fIproxyproto\fP, this is the transport packet's destination, not the destination claimed in the PROXY header.
|
||||||
|
|
||||||
|
|
||||||
.SS "UTILITY FUNCTIONS"
|
.SS "UTILITY FUNCTIONS"
|
||||||
.IP \(bu 4
|
.IP \(bu 4
|
||||||
\fB\fCincidr(ip string, cidr string) bool\fR: returns true if \fIip\fP is within \fIcidr\fP
|
\fB\fCincidr(ip string, cidr string) bool\fR: returns true if \fIip\fP is within \fIcidr\fP
|
||||||
|
|||||||
@@ -200,6 +200,15 @@ func (a *Addr) Unwrap() net.Addr {
|
|||||||
return a.r
|
return a.r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LocalAddr returns the transport packet's destination when the underlying
|
||||||
|
// address carries one. It is independent of the PROXY header's destination.
|
||||||
|
func (a *Addr) LocalAddr() net.Addr {
|
||||||
|
if packet, ok := a.u.(interface{ LocalAddr() net.Addr }); ok {
|
||||||
|
return packet.LocalAddr()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// tlvsFor returns the header's raw TLVs when TLV capture is enabled, else nil.
|
// tlvsFor returns the header's raw TLVs when TLV capture is enabled, else nil.
|
||||||
func (c *PacketConn) tlvsFor(header *proxyproto.Header) []byte {
|
func (c *PacketConn) tlvsFor(header *proxyproto.Header) []byte {
|
||||||
if !c.CaptureTLVs {
|
if !c.CaptureTLVs {
|
||||||
|
|||||||
@@ -136,11 +136,16 @@ functions defined below.
|
|||||||
* `opcode() int`: query OPCODE
|
* `opcode() int`: query OPCODE
|
||||||
* `port() string`: client's port
|
* `port() string`: client's port
|
||||||
* `proto() string`: protocol used (tcp or udp)
|
* `proto() string`: protocol used (tcp or udp)
|
||||||
* `server_ip() string`: server's IP address; for IPv6 addresses these are enclosed in brackets: `[::1]`
|
* `server_ip() string`: server's destination IP address; IPv6 addresses have no brackets: `::1`
|
||||||
* `server_port() string` : server's port
|
* `server_port() string` : server's port
|
||||||
* `size() int`: request size in bytes
|
* `size() int`: request size in bytes
|
||||||
* `type() string`: type of the request (A, AAAA, TXT, ...)
|
* `type() string`: type of the request (A, AAAA, TXT, ...)
|
||||||
|
|
||||||
|
For wildcard UDP listeners, `server_ip()` uses the destination address of the individual packet
|
||||||
|
when the operating system supplies packet information. If that information is unavailable, including
|
||||||
|
with custom packet listeners that do not expose it, the listening address is used instead.
|
||||||
|
With *proxyproto*, this is the transport packet's destination, not the destination claimed in the PROXY header.
|
||||||
|
|
||||||
#### Utility Functions
|
#### Utility Functions
|
||||||
|
|
||||||
* `incidr(ip string, cidr string) bool`: returns true if _ip_ is within _cidr_
|
* `incidr(ip string, cidr string) bool`: returns true if _ip_ is within _cidr_
|
||||||
|
|||||||
237
test/view_server_ip_test.go
Normal file
237
test/view_server_ip_test.go
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows
|
||||||
|
|
||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestViewServerIP(t *testing.T) {
|
||||||
|
for _, tc := range []struct{ name, bind, target string }{
|
||||||
|
{"wildcard4", "0.0.0.0", "127.0.0.1"},
|
||||||
|
{"wildcard6", "::", "::1"},
|
||||||
|
{"dual_stack", "::", "127.0.0.1"},
|
||||||
|
{"bound4", "127.0.0.1", "127.0.0.1"},
|
||||||
|
{"bound6", "::1", "::1"},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if tc.name == "dual_stack" && (runtime.GOOS == "openbsd" || runtime.GOOS == "dragonfly") {
|
||||||
|
t.Skip("the kernel requires IPv6-only sockets")
|
||||||
|
}
|
||||||
|
if net.ParseIP(tc.bind).To4() == nil {
|
||||||
|
checkIPv6Loopback(t)
|
||||||
|
}
|
||||||
|
corefile := fmt.Sprintf(`.:0 {
|
||||||
|
bind %s
|
||||||
|
view destination {
|
||||||
|
expr server_ip() == '%s' && name() == 'destination.example.'
|
||||||
|
}
|
||||||
|
template IN TXT . {
|
||||||
|
answer "{{ .Name }} 60 IN TXT \"matched\""
|
||||||
|
}
|
||||||
|
}`, tc.bind, tc.target)
|
||||||
|
instance, udp, tcp, err := CoreDNSServerAndPorts(corefile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { instance.Stop() })
|
||||||
|
for _, network := range []string{"udp", "tcp"} {
|
||||||
|
t.Run(network, func(t *testing.T) {
|
||||||
|
listener := udp
|
||||||
|
if network == "tcp" {
|
||||||
|
listener = tcp
|
||||||
|
}
|
||||||
|
_, port, err := net.SplitHostPort(listener)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
client := &dns.Client{Net: network, Timeout: 2 * time.Second}
|
||||||
|
conn, err := client.Dial(net.JoinHostPort(tc.target, port))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
if network == "udp" {
|
||||||
|
waitForViewUDP(t, net.JoinHostPort(tc.target, port))
|
||||||
|
}
|
||||||
|
for range 2 {
|
||||||
|
for _, name := range []string{"destination.example.", "unmatched.example."} {
|
||||||
|
answer, _, err := client.ExchangeWithConn(new(dns.Msg).SetQuestion(name, dns.TypeTXT), conn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := dns.RcodeRefused
|
||||||
|
if name == "destination.example." {
|
||||||
|
want = dns.RcodeSuccess
|
||||||
|
}
|
||||||
|
if answer.Rcode != want {
|
||||||
|
t.Errorf("query %s to %s: rcode=%s, want %s", name, tc.target, dns.RcodeToString[answer.Rcode], dns.RcodeToString[want])
|
||||||
|
}
|
||||||
|
if want == dns.RcodeSuccess && (len(answer.Answer) != 1 || answer.Answer[0].String() != name+"\t60\tIN\tTXT\t\"matched\"") {
|
||||||
|
t.Errorf("unexpected answer: %v", answer.Answer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkIPv6Loopback(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
conn, err := net.ListenPacket("udp6", "[::1]:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("IPv6 loopback is unavailable: %v", err)
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caddy returns before the serving goroutines start. Complete one exchange so
|
||||||
|
// assertions do not depend on packet information for a datagram queued before
|
||||||
|
// ServePacket enabled the socket options. The view may refuse this probe.
|
||||||
|
func waitForViewUDP(t *testing.T, address string) {
|
||||||
|
t.Helper()
|
||||||
|
client := &dns.Client{Net: "udp", Timeout: 2 * time.Second}
|
||||||
|
if _, _, err := client.Exchange(new(dns.Msg).SetQuestion("ready.invalid.", dns.TypeTXT), address); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestViewServerIPConcurrent(t *testing.T) {
|
||||||
|
if runtime.GOOS == "openbsd" || runtime.GOOS == "dragonfly" {
|
||||||
|
t.Skip("the kernel requires IPv6-only sockets")
|
||||||
|
}
|
||||||
|
checkIPv6Loopback(t)
|
||||||
|
instance, udp, _, err := CoreDNSServerAndPorts(`.:0 {
|
||||||
|
bind ::
|
||||||
|
view destination {
|
||||||
|
expr (server_ip() == '127.0.0.1' && name() == 'ipv4.example.') || (server_ip() == '::1' && name() == 'ipv6.example.')
|
||||||
|
}
|
||||||
|
template IN TXT . {
|
||||||
|
answer "{{ .Name }} 60 IN TXT \"matched\""
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { instance.Stop() })
|
||||||
|
_, port, err := net.SplitHostPort(udp)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
waitForViewUDP(t, net.JoinHostPort("127.0.0.1", port))
|
||||||
|
for _, tc := range []struct{ target, name string }{
|
||||||
|
{"127.0.0.1", "ipv4.example."},
|
||||||
|
{"::1", "ipv6.example."},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
client := &dns.Client{Net: "udp", Timeout: 2 * time.Second}
|
||||||
|
conn, err := client.Dial(net.JoinHostPort(tc.target, port))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
for range 50 {
|
||||||
|
answer, _, err := client.ExchangeWithConn(new(dns.Msg).SetQuestion(tc.name, dns.TypeTXT), conn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if answer.Rcode != dns.RcodeSuccess || len(answer.Answer) != 1 {
|
||||||
|
t.Fatalf("view for %s did not match: %v", tc.target, answer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Isolate request logging and Caddy restart callbacks from other integration
|
||||||
|
// tests. The child exercises the actual log plugin and its {local} formatter.
|
||||||
|
func TestViewServerIPLogAndReload(t *testing.T) {
|
||||||
|
bin, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
cmd := exec.CommandContext(ctx, bin, "-test.run=^TestViewServerIPLogHelper$", "-test.v")
|
||||||
|
cmd.Env = append(os.Environ(), "COREDNS_TEST_UDP_LOCAL=1", "GORACE=atexit_sleep_ms=0")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("log/reload process: %v\n%s", err, output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(output), "LOCAL=127.0.0.1") || strings.Contains(string(output), "LOCAL=0.0.0.0") {
|
||||||
|
t.Fatalf("incorrect request log destination:\n%s", output)
|
||||||
|
}
|
||||||
|
if runtime.GOOS != "windows" && !strings.Contains(string(output), "RELOADED=127.0.0.1") {
|
||||||
|
t.Fatalf("no destination log after listener reuse:\n%s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestViewServerIPLogHelper(t *testing.T) {
|
||||||
|
if os.Getenv("COREDNS_TEST_UDP_LOCAL") != "1" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clog.SetOutput(os.Stdout)
|
||||||
|
corefile := `.:0 {
|
||||||
|
bind 0.0.0.0
|
||||||
|
view destination {
|
||||||
|
expr server_ip() == '127.0.0.1'
|
||||||
|
}
|
||||||
|
log . "LOCAL={local}"
|
||||||
|
template IN TXT . {
|
||||||
|
answer "{{ .Name }} 60 IN TXT \"matched\""
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
instance, udp, _, err := CoreDNSServerAndPorts(corefile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() { instance.Stop() }()
|
||||||
|
_, port, err := net.SplitHostPort(udp)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
waitForViewUDP(t, net.JoinHostPort("127.0.0.1", port))
|
||||||
|
query := func() {
|
||||||
|
t.Helper()
|
||||||
|
client := &dns.Client{Net: "udp", Timeout: 2 * time.Second}
|
||||||
|
answer, _, err := client.Exchange(new(dns.Msg).SetQuestion("destination.example.", dns.TypeTXT), net.JoinHostPort("127.0.0.1", port))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if answer.Rcode != dns.RcodeSuccess || len(answer.Answer) != 1 {
|
||||||
|
t.Fatalf("destination view did not match: %v", answer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
query()
|
||||||
|
// Caddy cannot inherit Windows socket file descriptors on restart.
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Retain the configured :0 address so Caddy finds the old listener by
|
||||||
|
// its configuration key. A fresh listen would choose a different port.
|
||||||
|
corefile = strings.Replace(corefile, "LOCAL=", "RELOADED=", 1)
|
||||||
|
next, err := instance.Restart(NewInput(corefile))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
instance = next
|
||||||
|
if got, _ := CoreDNSServerPorts(instance, 0); got != udp {
|
||||||
|
t.Fatalf("listener was not reused: %s -> %s", udp, got)
|
||||||
|
}
|
||||||
|
query()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user