diff --git a/route_linux.go b/route_linux.go index c8c600d37..875d80e2f 100644 --- a/route_linux.go +++ b/route_linux.go @@ -919,12 +919,33 @@ func (h *Handle) routeHandleIter(route *Route, req *nl.NetlinkRequest, msg *nl.R return req.ExecuteIter(unix.NETLINK_ROUTE, 0, f) } +// gwIPFamily returns the IP family to use for a gateway address on the given +// route. A v4-mapped IPv6 gateway (::ffff:a.b.c.d, 16 bytes) reports FAMILY_V4 +// but is a valid V6 nexthop, so it is conformed to V6 only when the caller +// explicitly requested a V6 route via route.Family. This makes the behavior +// opt-in rather than silently reinterpreting an ambiguous address, and +// requiring 16 bytes leaves an explicit 4-byte IPv4 gateway reporting V4. +func gwIPFamily(route *Route, gw net.IP) int { + gwFamily := nl.GetIPFamily(gw) + if route.Family == FAMILY_V6 && gwFamily == FAMILY_V4 && len(gw) == net.IPv6len { + return FAMILY_V6 + } + return gwFamily +} + func (h *Handle) prepareRouteReq(route *Route, req *nl.NetlinkRequest, msg *nl.RtMsg) error { if req.NlMsghdr.Type != unix.RTM_GETROUTE && (route.Dst == nil || route.Dst.IP == nil) && route.Src == nil && route.Gw == nil && route.MPLSDst == nil { return fmt.Errorf("either Dst.IP, Src.IP or Gw must be set") } family := -1 + if route.Family != 0 { + // AF_UNSPEC (0) means unset. Honor an explicitly requested family so + // that ambiguous addresses (e.g. a v4-mapped IPv6 gateway, which is + // byte-identical to its IPv4 form as a net.IP) are encoded for the + // intended family even when no destination pins it down. + family = route.Family + } var rtAttrs []*nl.RtAttr if route.Dst != nil && route.Dst.IP != nil { @@ -996,7 +1017,7 @@ func (h *Handle) prepareRouteReq(route *Route, req *nl.NetlinkRequest, msg *nl.R } if route.Gw != nil { - gwFamily := nl.GetIPFamily(route.Gw) + gwFamily := gwIPFamily(route, route.Gw) if family != -1 && family != gwFamily { return fmt.Errorf("gateway, source, and destination ip are not the same IP family") } @@ -1030,7 +1051,7 @@ func (h *Handle) prepareRouteReq(route *Route, req *nl.NetlinkRequest, msg *nl.R } children := []nl.NetlinkRequestData{} if nh.Gw != nil { - gwFamily := nl.GetIPFamily(nh.Gw) + gwFamily := gwIPFamily(route, nh.Gw) if family != -1 && family != gwFamily { return fmt.Errorf("gateway, source, and destination ip are not the same IP family") } diff --git a/route_test.go b/route_test.go index 2f4d61524..e0fc0ec16 100644 --- a/route_test.go +++ b/route_test.go @@ -2977,3 +2977,183 @@ func TestRouteNHID(t *testing.T) { t.Fatalf("Expected route NHID %d, got %d", nh.ID, routes[0].NHID) } } + +// findRtAttr returns the Data of the first top-level RtAttr of the given type +// in req, failing the test if it is absent. +func findRtAttr(t *testing.T, req *nl.NetlinkRequest, attrType uint16) []byte { + t.Helper() + for _, d := range req.Data { + if attr, ok := d.(*nl.RtAttr); ok && attr.Type == attrType { + return attr.Data + } + } + t.Fatalf("attribute type %d not found in request", attrType) + return nil +} + +// TestPrepareRouteReqV4MappedV6Gateway verifies that a v4-mapped IPv6 gateway +// (::ffff:a.b.c.d) is encoded as a 16-byte AF_INET6 nexthop when the caller +// opts in by setting route.Family to FAMILY_V6. As a net.IP the gateway is +// byte-identical to its IPv4 form, so GetIPFamily reports FAMILY_V4; the +// explicit family is what enables the V6 encoding. This does not need a live +// netlink socket: prepareRouteReq only builds the request. +func TestPrepareRouteReqV4MappedV6Gateway(t *testing.T) { + gw := net.ParseIP("::ffff:192.0.2.1") + _, v6dst, err := net.ParseCIDR("2001:db8::/64") + if err != nil { + t.Fatal(err) + } + + for _, tt := range []struct { + name string + route *Route + }{ + { + name: "explicit V6 family, no destination", + route: &Route{Family: FAMILY_V6, Gw: gw}, + }, + { + name: "explicit V6 family with v6 destination", + route: &Route{Family: FAMILY_V6, Dst: v6dst, Gw: gw}, + }, + } { + t.Run(tt.name, func(t *testing.T) { + req := nl.NewNetlinkRequest(unix.RTM_NEWROUTE, unix.NLM_F_CREATE|unix.NLM_F_EXCL|unix.NLM_F_ACK) + msg := nl.NewRtMsg() + + if err := (&Handle{}).prepareRouteReq(tt.route, req, msg); err != nil { + t.Fatalf("prepareRouteReq: %v", err) + } + + if msg.Family != unix.AF_INET6 { + t.Fatalf("msg.Family = %d, want AF_INET6 (%d)", msg.Family, unix.AF_INET6) + } + + gwData := findRtAttr(t, req, unix.RTA_GATEWAY) + if len(gwData) != net.IPv6len { + t.Fatalf("RTA_GATEWAY length = %d, want %d", len(gwData), net.IPv6len) + } + if !net.IP(gwData).Equal(gw) { + t.Fatalf("RTA_GATEWAY = %v, want %v", net.IP(gwData), gw) + } + }) + } +} + +// TestPrepareRouteReqV4MappedV6GatewayMultiPath is the multipath analogue of +// TestPrepareRouteReqV4MappedV6Gateway: a v4-mapped IPv6 gateway carried in a +// MultiPath NexthopInfo must be encoded as a 16-byte AF_INET6 nexthop when the +// caller opts in via route.Family, matching the direct Route.Gw behavior. +func TestPrepareRouteReqV4MappedV6GatewayMultiPath(t *testing.T) { + gw := net.ParseIP("::ffff:192.0.2.1") + _, v6dst, err := net.ParseCIDR("2001:db8::/64") + if err != nil { + t.Fatal(err) + } + + route := &Route{ + Family: FAMILY_V6, + Dst: v6dst, + MultiPath: []*NexthopInfo{ + {LinkIndex: 1, Gw: gw}, + }, + } + + req := nl.NewNetlinkRequest(unix.RTM_NEWROUTE, unix.NLM_F_CREATE|unix.NLM_F_EXCL|unix.NLM_F_ACK) + msg := nl.NewRtMsg() + + if err := (&Handle{}).prepareRouteReq(route, req, msg); err != nil { + t.Fatalf("prepareRouteReq: %v", err) + } + + if msg.Family != unix.AF_INET6 { + t.Fatalf("msg.Family = %d, want AF_INET6 (%d)", msg.Family, unix.AF_INET6) + } + + // Descend into the nested RTA_MULTIPATH -> RtNexthop -> RTA_GATEWAY. + mp := findRtAttr(t, req, unix.RTA_MULTIPATH) + if len(mp) < unix.SizeofRtNexthop { + t.Fatalf("RTA_MULTIPATH too short: %d bytes", len(mp)) + } + nh := nl.DeserializeRtNexthop(mp) + attrs, err := nl.ParseRouteAttr(mp[unix.SizeofRtNexthop:int(nh.RtNexthop.Len)]) + if err != nil { + t.Fatalf("ParseRouteAttr: %v", err) + } + var gwData []byte + for _, attr := range attrs { + if attr.Attr.Type == unix.RTA_GATEWAY { + gwData = attr.Value + } + } + if gwData == nil { + t.Fatal("RTA_GATEWAY not found in multipath nexthop") + } + if len(gwData) != net.IPv6len { + t.Fatalf("RTA_GATEWAY length = %d, want %d", len(gwData), net.IPv6len) + } + if !net.IP(gwData).Equal(gw) { + t.Fatalf("RTA_GATEWAY = %v, want %v", net.IP(gwData), gw) + } +} + +// TestPrepareRouteReqV4MappedV6GatewayRequiresFamily verifies that the conform +// is opt-in: without an explicit route.Family, a v4-mapped gateway even +// alongside a V6 destination is treated as FAMILY_V4 and rejected, exactly as +// it was before v4-mapped nexthops were supported. +func TestPrepareRouteReqV4MappedV6GatewayRequiresFamily(t *testing.T) { + _, dst, err := net.ParseCIDR("2001:db8::/64") + if err != nil { + t.Fatal(err) + } + + req := nl.NewNetlinkRequest(unix.RTM_NEWROUTE, unix.NLM_F_CREATE|unix.NLM_F_EXCL|unix.NLM_F_ACK) + msg := nl.NewRtMsg() + + route := &Route{Dst: dst, Gw: net.ParseIP("::ffff:192.0.2.1")} + if err := (&Handle{}).prepareRouteReq(route, req, msg); err == nil { + t.Fatal("expected an error for a v4-mapped gateway without Family set, got nil") + } +} + +// TestPrepareRouteReqV4GatewayUnaffected guards against regressing the common +// case: a plain IPv4 gateway must still be encoded as a 4-byte AF_INET nexthop. +func TestPrepareRouteReqV4GatewayUnaffected(t *testing.T) { + _, dst, err := net.ParseCIDR("192.0.2.0/24") + if err != nil { + t.Fatal(err) + } + gw := net.ParseIP("192.0.2.1") + + req := nl.NewNetlinkRequest(unix.RTM_NEWROUTE, unix.NLM_F_CREATE|unix.NLM_F_EXCL|unix.NLM_F_ACK) + msg := nl.NewRtMsg() + + if err := (&Handle{}).prepareRouteReq(&Route{Dst: dst, Gw: gw}, req, msg); err != nil { + t.Fatalf("prepareRouteReq: %v", err) + } + + if msg.Family != unix.AF_INET { + t.Fatalf("msg.Family = %d, want AF_INET (%d)", msg.Family, unix.AF_INET) + } + + gwData := findRtAttr(t, req, unix.RTA_GATEWAY) + if len(gwData) != net.IPv4len { + t.Fatalf("RTA_GATEWAY length = %d, want %d", len(gwData), net.IPv4len) + } +} + +// TestPrepareRouteReqExplicitV4GatewayOnV6Route verifies that even with an +// explicit V6 family, a 4-byte IPv4 gateway is not conformed to V6: the conform +// requires a 16-byte slice, so a deliberately 4-byte address ("I really meant +// IPv4") still errors rather than being silently reinterpreted as a mapped +// nexthop. +func TestPrepareRouteReqExplicitV4GatewayOnV6Route(t *testing.T) { + gw := net.ParseIP("192.0.2.1").To4() // explicit 4-byte IPv4 + + req := nl.NewNetlinkRequest(unix.RTM_NEWROUTE, unix.NLM_F_CREATE|unix.NLM_F_EXCL|unix.NLM_F_ACK) + msg := nl.NewRtMsg() + + if err := (&Handle{}).prepareRouteReq(&Route{Family: FAMILY_V6, Gw: gw}, req, msg); err == nil { + t.Fatal("expected an error for a 4-byte IPv4 gateway on a V6 route, got nil") + } +}