From 1936a2bb40cd8e917011c0cdc55f42fb573b8ed3 Mon Sep 17 00:00:00 2001 From: Jaime Hablutzel Date: Thu, 25 Jun 2026 17:49:32 -0500 Subject: [PATCH] fix(dnstap): store IPv4-mapped IPv6 addresses as 4 octets with SocketFamily INET (#8186) Signed-off-by: Jaime Hablutzel --- plugin/dnstap/handler_test.go | 5 +- plugin/dnstap/msg/msg.go | 35 +++++++------- plugin/dnstap/msg/msg_test.go | 91 +++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 20 deletions(-) create mode 100644 plugin/dnstap/msg/msg_test.go diff --git a/plugin/dnstap/handler_test.go b/plugin/dnstap/handler_test.go index a0a609cd2..9d589128e 100644 --- a/plugin/dnstap/handler_test.go +++ b/plugin/dnstap/handler_test.go @@ -123,8 +123,9 @@ func testMessage() *tap.Message { return &tap.Message{ SocketFamily: &inet, SocketProtocol: &udp, - QueryAddress: net.ParseIP("10.240.0.1"), - QueryPort: &port, + // Explicit 4-octet form, because that's the expected dnstap message representation when SocketFamily is INET. + QueryAddress: net.ParseIP("10.240.0.1").To4(), + QueryPort: &port, } } diff --git a/plugin/dnstap/msg/msg.go b/plugin/dnstap/msg/msg.go index 9cbc09252..0ff3036a8 100644 --- a/plugin/dnstap/msg/msg.go +++ b/plugin/dnstap/msg/msg.go @@ -15,31 +15,37 @@ var ( familyINET6 = tap.SocketFamily_INET6 ) +// socketFamilyAndAddress maps an IP to the dnstap SocketFamily and the address +// bytes to store for it. IPv4, including IPv4-mapped IPv6, is returned as a +// 4-octet INET address; anything else as a 16-octet INET6 address, matching +// https://github.com/dnstap/dnstap.pb/blob/main/dnstap.proto. +func socketFamilyAndAddress(ip net.IP) (*tap.SocketFamily, []byte) { + if ip4 := ip.To4(); ip4 != nil { + return &familyINET, ip4 + } + return &familyINET6, ip +} + +// TODO: SetQueryAddress and SetResponseAddress each set the message-level SocketFamily (and SocketProtocol) from their own address. A dnstap Message describes a single socket whose two endpoints share one family, but calling both setters with addresses of different families leaves SocketFamily reflecting only the last call and inconsistent with the other stored address. Evaluate replacing the two setters with a single SetAddresses(t, query, response) that derives SocketFamily/SocketProtocol once for the whole socket and rejects a family mismatch. + // SetQueryAddress adds the query address to the message. This also sets the SocketFamily and SocketProtocol. func SetQueryAddress(t *tap.Message, addr net.Addr) error { - t.SocketFamily = &familyINET switch a := addr.(type) { case *net.TCPAddr: t.SocketProtocol = &protoTCP - t.QueryAddress = a.IP p := uint32(a.Port) // #nosec G115 -- Port is inherently bounded (1-65535) t.QueryPort = &p - if a.IP.To4() == nil { - t.SocketFamily = &familyINET6 - } + t.SocketFamily, t.QueryAddress = socketFamilyAndAddress(a.IP) return nil case *net.UDPAddr: t.SocketProtocol = &protoUDP - t.QueryAddress = a.IP p := uint32(a.Port) // #nosec G115 -- Port is inherently bounded (1-65535) t.QueryPort = &p - if a.IP.To4() == nil { - t.SocketFamily = &familyINET6 - } + t.SocketFamily, t.QueryAddress = socketFamilyAndAddress(a.IP) return nil default: return fmt.Errorf("unknown address type: %T", a) @@ -48,29 +54,22 @@ func SetQueryAddress(t *tap.Message, addr net.Addr) error { // SetResponseAddress the response address to the message. This also sets the SocketFamily and SocketProtocol. func SetResponseAddress(t *tap.Message, addr net.Addr) error { - t.SocketFamily = &familyINET switch a := addr.(type) { case *net.TCPAddr: t.SocketProtocol = &protoTCP - t.ResponseAddress = a.IP p := uint32(a.Port) // #nosec G115 -- Port is inherently bounded (1-65535) t.ResponsePort = &p - if a.IP.To4() == nil { - t.SocketFamily = &familyINET6 - } + t.SocketFamily, t.ResponseAddress = socketFamilyAndAddress(a.IP) return nil case *net.UDPAddr: t.SocketProtocol = &protoUDP - t.ResponseAddress = a.IP p := uint32(a.Port) // #nosec G115 -- Port is inherently bounded (1-65535) t.ResponsePort = &p - if a.IP.To4() == nil { - t.SocketFamily = &familyINET6 - } + t.SocketFamily, t.ResponseAddress = socketFamilyAndAddress(a.IP) return nil default: return fmt.Errorf("unknown address type: %T", a) diff --git a/plugin/dnstap/msg/msg_test.go b/plugin/dnstap/msg/msg_test.go new file mode 100644 index 000000000..c6fb85622 --- /dev/null +++ b/plugin/dnstap/msg/msg_test.go @@ -0,0 +1,91 @@ +package msg + +import ( + "net" + "testing" + + tap "github.com/dnstap/golang-dnstap" +) + +// ipv4MappedINET is the 16-byte IPv4-mapped IPv6 form of 192.0.2.1 +// (::ffff:192.0.2.1). Dual-stack listeners report IPv4 clients this way, and +// Go's net.IP.To4 still recognizes them as IPv4. +var ipv4MappedINET = net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 192, 0, 2, 1} + +// An IPv4 client reported as an IPv4-mapped IPv6 address must be stored as a +// 4-octet address with SocketFamily INET, otherwise the dnstap message parser +// could get confused when expecting only 4 bytes but is presented with 16 bytes. +func TestSetAddressIPv4MappedReportedAsINET(t *testing.T) { + assertINET := func(t *testing.T, family tap.SocketFamily, addr []byte) { + t.Helper() + if family != tap.SocketFamily_INET { + t.Errorf("SocketFamily = %v, want INET", family) + } + if len(addr) != 4 { + t.Errorf("address length = %d, want 4 octets for INET", len(addr)) + } + if got := net.IP(addr).String(); got != "192.0.2.1" { + t.Errorf("address = %q, want %q", got, "192.0.2.1") + } + } + + t.Run("query/udp", func(t *testing.T) { + m := new(tap.Message) + if err := SetQueryAddress(m, &net.UDPAddr{IP: ipv4MappedINET}); err != nil { + t.Fatal(err) + } + assertINET(t, m.GetSocketFamily(), m.GetQueryAddress()) + }) + + t.Run("query/tcp", func(t *testing.T) { + m := new(tap.Message) + if err := SetQueryAddress(m, &net.TCPAddr{IP: ipv4MappedINET}); err != nil { + t.Fatal(err) + } + assertINET(t, m.GetSocketFamily(), m.GetQueryAddress()) + }) + + t.Run("response/udp", func(t *testing.T) { + m := new(tap.Message) + if err := SetResponseAddress(m, &net.UDPAddr{IP: ipv4MappedINET}); err != nil { + t.Fatal(err) + } + assertINET(t, m.GetSocketFamily(), m.GetResponseAddress()) + }) + + t.Run("response/tcp", func(t *testing.T) { + m := new(tap.Message) + if err := SetResponseAddress(m, &net.TCPAddr{IP: ipv4MappedINET}); err != nil { + t.Fatal(err) + } + assertINET(t, m.GetSocketFamily(), m.GetResponseAddress()) + }) +} + +// Native IPv4 addresses keep SocketFamily INET and a 4-octet address. +func TestSetAddressNativeIPv4(t *testing.T) { + m := new(tap.Message) + if err := SetQueryAddress(m, &net.UDPAddr{IP: net.IP{192, 0, 2, 1}}); err != nil { + t.Fatal(err) + } + if m.GetSocketFamily() != tap.SocketFamily_INET { + t.Errorf("SocketFamily = %v, want INET", m.GetSocketFamily()) + } + if got := len(m.GetQueryAddress()); got != 4 { + t.Errorf("address length = %d, want 4 octets for INET", got) + } +} + +// Genuine IPv6 addresses keep SocketFamily INET6 and a 16-octet address. +func TestSetAddressNativeIPv6(t *testing.T) { + m := new(tap.Message) + if err := SetQueryAddress(m, &net.UDPAddr{IP: net.ParseIP("2001:db8::1")}); err != nil { + t.Fatal(err) + } + if m.GetSocketFamily() != tap.SocketFamily_INET6 { + t.Errorf("SocketFamily = %v, want INET6", m.GetSocketFamily()) + } + if got := len(m.GetQueryAddress()); got != 16 { + t.Errorf("address length = %d, want 16 octets for INET6", got) + } +}