From 23518c3e2e58ad2ee2dd21f22da2d2a5a9549e93 Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Mon, 3 Nov 2025 21:09:04 +0100 Subject: [PATCH] common/helpers: add a NetIPTo6() function This should be netip.To6() but it does not exist and it was rejected. There is a benchmark showing the improvment of such optimisation: BenchmarkNetIPTo6/safe_v4-12 170152954 7.054 ns/op BenchmarkNetIPTo6/unsafe_v4-12 764772190 1.553 ns/op See https://github.com/golang/go/issues/54365. --- common/helpers/cache/cache_test.go | 2 +- common/helpers/ipv6.go | 34 ++++++ common/helpers/ipv6_test.go | 116 +++++++++++++++++++ common/helpers/subnetmap.go | 2 +- outlet/core/root_test.go | 2 +- outlet/flow/decoder/helpers.go | 2 +- outlet/metadata/cache_test.go | 2 +- outlet/metadata/provider/snmp/root.go | 6 +- outlet/metadata/root_test.go | 2 +- outlet/routing/provider/bioris/root.go | 5 +- outlet/routing/provider/bmp/prefixes_test.go | 6 +- 11 files changed, 165 insertions(+), 14 deletions(-) create mode 100644 common/helpers/ipv6.go create mode 100644 common/helpers/ipv6_test.go diff --git a/common/helpers/cache/cache_test.go b/common/helpers/cache/cache_test.go index 59ff8713..457b2bd4 100644 --- a/common/helpers/cache/cache_test.go +++ b/common/helpers/cache/cache_test.go @@ -15,7 +15,7 @@ import ( func expectCacheGet(t *testing.T, c *cache.Cache[netip.Addr, string], key string, expectedResult string, expectedOk bool) { t.Helper() ip := netip.MustParseAddr(key) - ip = netip.AddrFrom16(ip.As16()) + ip = helpers.NetIPTo6(ip) result, ok := c.Get(time.Time{}, ip) got := struct { Result string diff --git a/common/helpers/ipv6.go b/common/helpers/ipv6.go new file mode 100644 index 00000000..afd9c910 --- /dev/null +++ b/common/helpers/ipv6.go @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2025 Free Mobile +// SPDX-License-Identifier: AGPL-3.0-only + +package helpers + +import ( + "net/netip" + "unsafe" +) + +var ( + someIPv6 = netip.MustParseAddr("2001:db8::1") + z6noz = *(*uint64)(unsafe.Add(unsafe.Pointer(&someIPv6), 16)) +) + +// NetIPTo6 maps an IPv4 to an IPv4-mapped IPv6 and returns an IPv6 unmodified. +// This is unsafe, but there is a test to ensure netip.Addr is like we expect. +// Copying a unique.Handle bypass reference count, but z6noz is "static". +// +// This would be trivial to implement inside netip: +// +// func (ip Addr) Unmap() Addr { +// if ip.Is4() { +// ip.z = z6noz +// } +// return ip +// } +func NetIPTo6(ip netip.Addr) netip.Addr { + if ip.Is4() { + p := (*uint64)(unsafe.Add(unsafe.Pointer(&ip), 16)) + *p = z6noz + } + return ip +} diff --git a/common/helpers/ipv6_test.go b/common/helpers/ipv6_test.go new file mode 100644 index 00000000..3c5a767e --- /dev/null +++ b/common/helpers/ipv6_test.go @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: 2025 Free Mobile +// SPDX-License-Identifier: AGPL-3.0-only + +package helpers_test + +import ( + "fmt" + "net/netip" + "reflect" + "testing" + "unsafe" + + "akvorado/common/helpers" +) + +func TestNetIPTo6(t *testing.T) { + cases := []struct { + input netip.Addr + output netip.Addr + }{ + {netip.Addr{}, netip.Addr{}}, + {netip.MustParseAddr("192.168.1.1"), netip.MustParseAddr("::ffff:192.168.1.1")}, + {netip.MustParseAddr("2a01:db8::1"), netip.MustParseAddr("2a01:db8::1")}, + } + for _, tc := range cases { + got := helpers.NetIPTo6(tc.input) + if diff := helpers.Diff(got, tc.output); diff != "" { + t.Errorf("NetIPTo6(%s) (-got, +want):\n%s", tc.input, diff) + } + } +} + +func TestNetIPAddrStructure(t *testing.T) { + var addr netip.Addr + addrType := reflect.TypeOf(addr) + + // Test total size: 24 bytes (16 for uint128 + 8 for unique.Handle) + if unsafe.Sizeof(addr) != 24 { + t.Errorf("netip.Addr size = %d, want 24", unsafe.Sizeof(addr)) + } + + // Test number of fields + if addrType.NumField() != 2 { + t.Errorf("netip.Addr has %d fields, want 2", addrType.NumField()) + } + + // Test field 0: addr (uint128, 16 bytes) + field0 := addrType.Field(0) + if field0.Name != "addr" { + t.Errorf("field 0 name = %q, want %q", field0.Name, "addr") + } + if field0.Type.String() != "netip.uint128" { + t.Errorf("field 0 type = %q, want %q", field0.Type.String(), "netip.uint128") + } + if field0.Offset != 0 { + t.Errorf("field 0 offset = %d, want 0", field0.Offset) + } + if field0.Type.Size() != 16 { + t.Errorf("field 0 (addr) size = %d, want 16", field0.Type.Size()) + } + + // Test field 1: z (unique.Handle, 8 bytes) + field1 := addrType.Field(1) + if field1.Name != "z" { + t.Errorf("field 1 name = %q, want %q", field1.Name, "z") + } + if field1.Type.String() != "unique.Handle[net/netip.addrDetail]" { + t.Errorf("field 0 type = %q, want %q", field1.Type.String(), "unique.Handle[net/netip.addrDetail]") + } + if field1.Offset != 16 { + t.Errorf("field 1 offset = %d, want 16", field1.Offset) + } + if field1.Type.Size() != 8 { + t.Errorf("field 1 (z) size = %d, want 8", field1.Type.Size()) + } + + t.Logf("netip.Addr structure verified: [addr %d bytes @ 0] [z %d bytes @ 16]", + field0.Type.Size(), field1.Type.Size()) +} + +func netIPTo6Safe(ip netip.Addr) netip.Addr { + if ip.Is4() { + return netip.AddrFrom16(ip.As16()) + } + return ip +} + +func netIPTo6SafeNocheck(ip netip.Addr) netip.Addr { + return netip.AddrFrom16(ip.As16()) +} + +func BenchmarkNetIPTo6(b *testing.B) { + ipv4 := netip.MustParseAddr("192.168.1.1") + ipv6 := netip.MustParseAddr("2a01:db8::1") + for _, ip := range []netip.Addr{ipv4, ipv6} { + version := "v4" + if ip.Is6() { + version = "v6" + } + b.Run(fmt.Sprintf("safe %s", version), func(b *testing.B) { + for b.Loop() { + _ = netIPTo6Safe(ip) + } + }) + b.Run(fmt.Sprintf("safe nocheck %s", version), func(b *testing.B) { + for b.Loop() { + _ = netIPTo6SafeNocheck(ip) + } + }) + b.Run(fmt.Sprintf("unsafe %s", version), func(b *testing.B) { + for b.Loop() { + _ = helpers.NetIPTo6(ip) + } + }) + } +} diff --git a/common/helpers/subnetmap.go b/common/helpers/subnetmap.go index d115dd9a..34d307d5 100644 --- a/common/helpers/subnetmap.go +++ b/common/helpers/subnetmap.go @@ -228,7 +228,7 @@ func PrefixTo16(prefix netip.Prefix) netip.Prefix { return prefix } // Convert IPv4 to IPv4-mapped IPv6 - return netip.PrefixFrom(netip.AddrFrom16(prefix.Addr().As16()), prefix.Bits()+96) + return netip.PrefixFrom(NetIPTo6(prefix.Addr()), prefix.Bits()+96) } // SubnetMapParseKey parses a prefix or an IP address into a netip.Prefix that diff --git a/outlet/core/root_test.go b/outlet/core/root_test.go index 60232cb5..4110b4f4 100644 --- a/outlet/core/root_test.go +++ b/outlet/core/root_test.go @@ -77,7 +77,7 @@ func TestCore(t *testing.T) { msg := &schema.FlowMessage{ TimeReceived: 200, SamplingRate: 1000, - ExporterAddress: netip.AddrFrom16(netip.MustParseAddr(exporter).As16()), + ExporterAddress: helpers.NetIPTo6(netip.MustParseAddr(exporter)), InIf: in, OutIf: out, SrcAddr: netip.MustParseAddr("::ffff:67.43.156.77"), diff --git a/outlet/flow/decoder/helpers.go b/outlet/flow/decoder/helpers.go index c12c68ba..8e9909a1 100644 --- a/outlet/flow/decoder/helpers.go +++ b/outlet/flow/decoder/helpers.go @@ -176,7 +176,7 @@ func ParseEthernet(sch *schema.Component, bf *schema.FlowMessage, data []byte) u // DecodeIP decodes an IP address func DecodeIP(b []byte) netip.Addr { if ip, ok := netip.AddrFromSlice(b); ok { - return netip.AddrFrom16(ip.As16()) + return helpers.NetIPTo6(ip) } return netip.Addr{} } diff --git a/outlet/metadata/cache_test.go b/outlet/metadata/cache_test.go index 1c9c7d2f..18a6019a 100644 --- a/outlet/metadata/cache_test.go +++ b/outlet/metadata/cache_test.go @@ -33,7 +33,7 @@ func setupTestCache(t *testing.T) (*reporter.Reporter, *metadataCache) { func expectCacheLookup(t *testing.T, sc *metadataCache, exporterIP string, ifIndex uint, expected provider.Answer) { t.Helper() ip := netip.MustParseAddr(exporterIP) - ip = netip.AddrFrom16(ip.As16()) + ip = helpers.NetIPTo6(ip) got, ok := sc.Lookup(time.Time{}, provider.Query{ ExporterIP: ip, IfIndex: ifIndex, diff --git a/outlet/metadata/provider/snmp/root.go b/outlet/metadata/provider/snmp/root.go index b19c1742..fbce4d10 100644 --- a/outlet/metadata/provider/snmp/root.go +++ b/outlet/metadata/provider/snmp/root.go @@ -7,9 +7,9 @@ package snmp import ( "context" - "net/netip" "time" + "akvorado/common/helpers" "akvorado/common/reporter" "akvorado/outlet/metadata/provider" ) @@ -38,8 +38,8 @@ func (configuration Configuration) New(_ context.Context, r *reporter.Reporter) for exporterIP, agentIP := range configuration.Agents { if exporterIP.Is4() || agentIP.Is4() { delete(configuration.Agents, exporterIP) - exporterIP = netip.AddrFrom16(exporterIP.As16()) - agentIP = netip.AddrFrom16(agentIP.As16()) + exporterIP = helpers.NetIPTo6(exporterIP) + agentIP = helpers.NetIPTo6(agentIP) configuration.Agents[exporterIP] = agentIP } } diff --git a/outlet/metadata/root_test.go b/outlet/metadata/root_test.go index 50c62caf..167912c7 100644 --- a/outlet/metadata/root_test.go +++ b/outlet/metadata/root_test.go @@ -22,7 +22,7 @@ import ( func expectMockLookup(t *testing.T, c *Component, exporter string, ifIndex uint, expected provider.Answer) { t.Helper() - ip := netip.AddrFrom16(netip.MustParseAddr(exporter).As16()) + ip := helpers.NetIPTo6(netip.MustParseAddr(exporter)) got := c.Lookup(time.Now(), ip, ifIndex) if diff := helpers.Diff(got, expected); diff != "" { t.Fatalf("Lookup() (-got, +want):\n%s", diff) diff --git a/outlet/routing/provider/bioris/root.go b/outlet/routing/provider/bioris/root.go index 1991f691..6758f6e2 100644 --- a/outlet/routing/provider/bioris/root.go +++ b/outlet/routing/provider/bioris/root.go @@ -25,6 +25,7 @@ import ( "google.golang.org/grpc/credentials/insecure" "gopkg.in/tomb.v2" + "akvorado/common/helpers" "akvorado/common/reporter" "akvorado/outlet/routing/provider" "akvorado/outlet/routing/provider/bmp" @@ -183,7 +184,7 @@ func (p *Provider) Refresh(ctx context.Context) { p.r.Err(err).Msgf("error while parsing router address %s", router.Address) continue } - routerAddress = netip.AddrFrom16(routerAddress.As16()) + routerAddress = helpers.NetIPTo6(routerAddress) routers[routerAddress] = append(routers[routerAddress], p.instances[config.GRPCAddr]) p.metrics.knownRouters.WithLabelValues(config.GRPCAddr).Inc() @@ -318,7 +319,7 @@ func (p *Provider) lpmResponseToLookupResult(lpm *pb.LPMResponse) (bmp.LookupRes if !ok { return res, errInvalidNextHop } - res.NextHop = nhAddr + res.NextHop = helpers.NetIPTo6(nhAddr) } return res, nil diff --git a/outlet/routing/provider/bmp/prefixes_test.go b/outlet/routing/provider/bmp/prefixes_test.go index 86b93a83..32c54cb9 100644 --- a/outlet/routing/provider/bmp/prefixes_test.go +++ b/outlet/routing/provider/bmp/prefixes_test.go @@ -239,7 +239,7 @@ func BenchmarkRIBInsertion(b *testing.B) { if prng2[p].IntN(10) == 0 { continue } - pfx := netip.PrefixFrom(netip.AddrFrom16(r.Prefix.Addr().As16()), r.Prefix.Bits()+96) + pfx := netip.PrefixFrom(helpers.NetIPTo6(r.Prefix.Addr()), r.Prefix.Bits()+96) tentative++ inserted += rib.AddPrefix(pfx, route{ peer: uint32(p), @@ -295,7 +295,7 @@ func BenchmarkRIBLookup(b *testing.B) { if prng2[p].IntN(10) == 0 { continue } - pfx := netip.PrefixFrom(netip.AddrFrom16(r.Prefix.Addr().As16()), r.Prefix.Bits()+96) + pfx := netip.PrefixFrom(helpers.NetIPTo6(r.Prefix.Addr()), r.Prefix.Bits()+96) rib.AddPrefix(pfx, route{ peer: uint32(p), nlri: rib.nlris.Put(nlri{family: bgp.RF_IPv4_UC}), @@ -344,7 +344,7 @@ func BenchmarkRIBFlush(b *testing.B) { if prng2[p].IntN(10) == 0 { continue } - pfx := netip.PrefixFrom(netip.AddrFrom16(r.Prefix.Addr().As16()), r.Prefix.Bits()+96) + pfx := netip.PrefixFrom(helpers.NetIPTo6(r.Prefix.Addr()), r.Prefix.Bits()+96) rib.AddPrefix(pfx, route{ peer: uint32(p), nlri: rib.nlris.Put(nlri{family: bgp.RF_IPv4_UC}),