From d530221105b32065159b3c7f208d1fce6ab99b91 Mon Sep 17 00:00:00 2001 From: astrobounce Date: Wed, 29 Jul 2026 12:19:37 +0530 Subject: [PATCH 1/8] Add support for Resilient Next-hop Groups Operations --- nexthop.go | 26 ++++++++++ nexthop_linux.go | 127 ++++++++++++++++++++++++++++++++++++++++++++++- nl/nl_linux.go | 15 ++++++ 3 files changed, 167 insertions(+), 1 deletion(-) diff --git a/nexthop.go b/nexthop.go index 4f961665a..911a3d2ad 100644 --- a/nexthop.go +++ b/nexthop.go @@ -7,12 +7,38 @@ import ( "strings" ) +const ( + // NEXTHOP_GRP_TYPE_MPATH is default multi-path hash threshold + NEXTHOP_GRP_TYPE_MPATH uint16 = iota + + // NEXTHOP_GRP_TYPE_RES is Resilient nexthop group + NEXTHOP_GRP_TYPE_RES +) + +// NexthopGroupMpath represents one member of a nexthtop group +type NexthopGroupMpath struct { + // ID of an existing nexthop to include in the group + ID uint16 + // Relative weight, 1-256. Zero is treated as 1 + Weight uint16 +} + +type NexthopResGroup struct { + Buckets uint16 + IdleTimer uint32 + UnbalancedTimer uint32 + UnbalancedTime uint64 +} + type Nexthop struct { ID uint32 Blackhole bool OIF uint32 Gateway net.IP Protocol RouteProtocol + Group []NexthopGroupMpath + GroupType uint16 + ResGroup *NexthopResGroup } func (h *Nexthop) String() string { diff --git a/nexthop_linux.go b/nexthop_linux.go index d9f0c6d6f..59dd99f8e 100644 --- a/nexthop_linux.go +++ b/nexthop_linux.go @@ -8,6 +8,17 @@ import ( "golang.org/x/sys/unix" ) +const ( + // sizeofNexthopGrp is the size of struct nexthop_grp from + // Linux uapi/linux/nexthop.h: u32 id, u8 weight, u8 resvd1, u16 resvd2. + sizeofNexthopGrp = 8 + + // nexthopResGroupUserHZ converts resilient group timer values between + // seconds (used in NexthopResGroup) and the clock_t ticks expected on + // the wire. USER_HZ is fixed at 100 in the Linux userspace ABI. + nexthopResGroupUserHZ = 100 +) + // NexthopAdd will add a nexthop to the system. // Equivalent to: `ip nexthop add $nexthop` func NexthopAdd(nh *Nexthop) error { @@ -158,6 +169,107 @@ var nexthopAttrHandlers = map[uint16]struct { } nh.OIF = native.Uint32(attr.Data[0:4]) }, + }, + unix.NHA_GROUP: { + encode: func(nh *Nexthop) *nl.RtAttr { + if len(nh.Group) == 0 { + return nil + } + b := make([]byte, sizeofNexthopGrp*len(nh.Group)) + for i, entry := range nh.Group { + // The kernel interprets the on-wire weight as + // "actual weight - 1", valid range 1-256. + w := entry.Weight + if w == 0 { + w = 1 + } else if w > 256 { + w = 256 + } + off := i * sizeofNexthopGrp + native.PutUint32(b[off:off+4], entry.ID) + b[off+4] = uint8(w - 1) + } + return nl.NewRtAttr(unix.NHA_GROUP, b) + }, + decode: func(nh *Nexthop, attr *nl.RtAttr) { + nh.Group = nil + for off := 0; off+sizeofNexthopGrp <= len(attr.Data); off += sizeofNexthopGrp { + nh.Group = append(nh.Group, NexthopGroupMpath{ + ID: native.Uint32(attr.Data[off : off+4]), + Weight: uint16(attr.Data[off+4]) + 1, + }) + } + }, + }, + unix.NHA_GROUP_TYPE: { + encode: func(nh *Nexthop) *nl.RtAttr { + if nh.GroupType != NEXTHOP_GRP_TYPE_MPATH { + b := make([]byte, 2) + native.PutUint16(b, nh.GroupType) + return nl.NewRtAttr(unix.NHA_GROUP_TYPE, b) + } + return nil + }, + decode: func(nh *Nexthop, attr *nl.RtAttr) { + if len(attr.Data) < 2 { + return + } + nh.GroupType = native.Uint16(attr.Data[0:2]) + }, + }, + nl.NHA_RES_GROUP: { + encode: func(nh *Nexthop) *nl.RtAttr { + if nh.ResGroup == nil { + return nil + } + // Strict netlink validation requires the NLA_F_NESTED + // flag on nested attributes. + attr := nl.NewRtAttr(nl.NHA_RES_GROUP|int(nl.NLA_F_NESTED), nil) + if nh.ResGroup.Buckets > 0 { + b := make([]byte, 2) + native.PutUint16(b, nh.ResGroup.Buckets) + attr.AddRtAttr(nl.NHA_RES_GROUP_BUCKETS, b) + } + if nh.ResGroup.IdleTimer > 0 { + b := make([]byte, 4) + native.PutUint32(b, nh.ResGroup.IdleTimer*nexthopResGroupUserHZ) + attr.AddRtAttr(nl.NHA_RES_GROUP_IDLE_TIMER, b) + } + if nh.ResGroup.UnbalancedTimer > 0 { + b := make([]byte, 4) + native.PutUint32(b, nh.ResGroup.UnbalancedTimer*nexthopResGroupUserHZ) + attr.AddRtAttr(nl.NHA_RES_GROUP_UNBALANCED_TIMER, b) + } + return attr + }, + decode: func(nh *Nexthop, attr *nl.RtAttr) { + nested, err := nl.ParseRouteAttr(attr.Data) + if err != nil { + return + } + res := &NexthopResGroup{} + for _, a := range nested { + switch a.Attr.Type & nl.NLA_TYPE_MASK { + case nl.NHA_RES_GROUP_BUCKETS: + if len(a.Value) >= 2 { + res.Buckets = native.Uint16(a.Value[0:2]) + } + case nl.NHA_RES_GROUP_IDLE_TIMER: + if len(a.Value) >= 4 { + res.IdleTimer = native.Uint32(a.Value[0:4]) / nexthopResGroupUserHZ + } + case nl.NHA_RES_GROUP_UNBALANCED_TIMER: + if len(a.Value) >= 4 { + res.UnbalancedTimer = native.Uint32(a.Value[0:4]) / nexthopResGroupUserHZ + } + case nl.NHA_RES_GROUP_UNBALANCED_TIME: + if len(a.Value) >= 8 { + res.UnbalancedTime = native.Uint64(a.Value[0:8]) / nexthopResGroupUserHZ + } + } + } + nh.ResGroup = res + }, }, unix.NHA_GATEWAY: { encode: func(nh *Nexthop) *nl.RtAttr { @@ -220,7 +332,7 @@ func parseNhmsg(m []byte) (*Nexthop, error) { rtAttrs := make([]*nl.RtAttr, 0, len(rawAttrs)) for _, rawAttr := range rawAttrs { - rtAttrs = append(rtAttrs, nl.NewRtAttr(int(rawAttr.Attr.Type), rawAttr.Value)) + rtAttrs = append(rtAttrs, nl.NewRtAttr(int(rawAttr.Attr.Type&nl.NLA_TYPE_MASK), rawAttr.Value)) } nh := &Nexthop{ @@ -233,6 +345,9 @@ func parseNhmsg(m []byte) (*Nexthop, error) { } func deriveFamilyFromNexthop(nh *Nexthop) uint8 { + if len(nh.Group) > 0 { + return uint8(FAMILY_ALL) + } if nh.Gateway == nil || nh.Gateway.To4() != nil { return FAMILY_V4 } @@ -240,6 +355,13 @@ func deriveFamilyFromNexthop(nh *Nexthop) uint8 { } func prepareNewNexthop(nh *Nexthop, req *nl.NetlinkRequest, msg *nl.Nhmsg) error { + if nh.ResGroup != nil && nh.GroupType != NEXTHOP_GRP_TYPE_RES { + return fmt.Errorf("nexthop: ResGroup requires GroupType to be NEXTHOP_GRP_TYPE_RES") + } + if nh.GroupType != NEXTHOP_GRP_TYPE_MPATH && len(nh.Group) == 0 { + return fmt.Errorf("nexthop: GroupType is set but Group is empty") + } + var rtAttrs []*nl.RtAttr // We can find the supported attributes from the kernel source code: @@ -255,6 +377,9 @@ func prepareNewNexthop(nh *Nexthop, req *nl.NetlinkRequest, msg *nl.Nhmsg) error unix.NHA_BLACKHOLE, unix.NHA_OIF, unix.NHA_GATEWAY, + unix.NHA_GROUP, + unix.NHA_GROUP_TYPE, + nl.NHA_RES_GROUP, })...) msg.Family = deriveFamilyFromNexthop(nh) diff --git a/nl/nl_linux.go b/nl/nl_linux.go index 263c8727b..198c5e495 100644 --- a/nl/nl_linux.go +++ b/nl/nl_linux.go @@ -35,6 +35,21 @@ const ( SizeofCnMsgOp = 0x18 ) +const ( + NHA_RES_GROUP = 12 // Nest containing attributes specific to resilient groups + NHA_RES_BUCKET = 13 // Nest containing attributes specific to buckets +) + +const ( + NHA_RES_GROUP_UNSPEC = iota + NHA_RES_GROUP_BUCKETS // u16: Number of buckets in the hash table + NHA_RES_GROUP_IDLE_TIMER // u32: Idle timer in units of clock_t + NHA_RES_GROUP_UNBALANCED_TIMER // u32: Unbalanced timer in units of clock_t + NHA_RES_GROUP_UNBALANCED_TIME // u64: Time out of balance (read-only for dumps) +) + +const NHA_RES_GROUP_PAD = NHA_RES_GROUP_UNSPEC + // SupportedNlFamilies contains the list of netlink families this netlink package supports var SupportedNlFamilies = []int{unix.NETLINK_ROUTE, unix.NETLINK_XFRM, unix.NETLINK_NETFILTER} From 99b1e1cb8b96fbe7a32b99cd5425aa32e0ad54ed Mon Sep 17 00:00:00 2001 From: Arvind Sharma Date: Sat, 1 Aug 2026 23:48:24 +0530 Subject: [PATCH 2/8] coderabbit review comment fixes --- nexthop.go | 32 ++++++++++++++++++-------------- nexthop_linux.go | 13 +++++-------- nl/nl_linux.go | 1 + 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/nexthop.go b/nexthop.go index 911a3d2ad..184da8100 100644 --- a/nexthop.go +++ b/nexthop.go @@ -7,38 +7,42 @@ import ( "strings" ) +// Nexthop group types - reference https://github.com/torvalds/linux/blob/master/include/uapi/linux/nexthop.h const ( - // NEXTHOP_GRP_TYPE_MPATH is default multi-path hash threshold - NEXTHOP_GRP_TYPE_MPATH uint16 = iota + // NEXTHOP_GRP_TYPE_MPATH is default multi-path hash threshold + NEXTHOP_GRP_TYPE_MPATH uint16 = iota - // NEXTHOP_GRP_TYPE_RES is Resilient nexthop group - NEXTHOP_GRP_TYPE_RES + // NEXTHOP_GRP_TYPE_RES is Resilient nexthop group + NEXTHOP_GRP_TYPE_RES ) // NexthopGroupMpath represents one member of a nexthtop group type NexthopGroupMpath struct { - // ID of an existing nexthop to include in the group - ID uint16 - // Relative weight, 1-256. Zero is treated as 1 - Weight uint16 + // ID of an existing nexthop to include in the group + ID uint32 + // Relative weight, 1-256. Zero is treated as 1 + Weight uint16 } +// NexthopResGroup is resilient nexthop group structure type NexthopResGroup struct { - Buckets uint16 - IdleTimer uint32 - UnbalancedTimer uint32 - UnbalancedTime uint64 + Buckets uint16 + IdleTimer uint32 + UnbalancedTimer uint32 + UnbalancedTime uint64 } +// Nexthop represent a nexthop object type Nexthop struct { ID uint32 Blackhole bool OIF uint32 Gateway net.IP Protocol RouteProtocol - Group []NexthopGroupMpath + // Nexthop group members for resilient nexthop group or multipath nexthop group + Group []NexthopGroupMpath GroupType uint16 - ResGroup *NexthopResGroup + ResGroup *NexthopResGroup } func (h *Nexthop) String() string { diff --git a/nexthop_linux.go b/nexthop_linux.go index 59dd99f8e..d0496ac2b 100644 --- a/nexthop_linux.go +++ b/nexthop_linux.go @@ -2,6 +2,7 @@ package netlink import ( "errors" + "fmt" "net" "github.com/vishvananda/netlink/nl" @@ -9,13 +10,10 @@ import ( ) const ( - // sizeofNexthopGrp is the size of struct nexthop_grp from - // Linux uapi/linux/nexthop.h: u32 id, u8 weight, u8 resvd1, u16 resvd2. + // sizeofNexthopGrp is size of single nexhthop group member sizeofNexthopGrp = 8 - // nexthopResGroupUserHZ converts resilient group timer values between - // seconds (used in NexthopResGroup) and the clock_t ticks expected on - // the wire. USER_HZ is fixed at 100 in the Linux userspace ABI. + // nexthopResGroupUserHZ is the userspace clock ticks per second nexthopResGroupUserHZ = 100 ) @@ -170,15 +168,14 @@ var nexthopAttrHandlers = map[uint16]struct { nh.OIF = native.Uint32(attr.Data[0:4]) }, }, - unix.NHA_GROUP: { + unix.NHA_GROUP: { encode: func(nh *Nexthop) *nl.RtAttr { if len(nh.Group) == 0 { return nil } b := make([]byte, sizeofNexthopGrp*len(nh.Group)) for i, entry := range nh.Group { - // The kernel interprets the on-wire weight as - // "actual weight - 1", valid range 1-256. + // Kernel interprets one weight wire = actual weight - 1 w := entry.Weight if w == 0 { w = 1 diff --git a/nl/nl_linux.go b/nl/nl_linux.go index 198c5e495..6f68f7de1 100644 --- a/nl/nl_linux.go +++ b/nl/nl_linux.go @@ -48,6 +48,7 @@ const ( NHA_RES_GROUP_UNBALANCED_TIME // u64: Time out of balance (read-only for dumps) ) +// NHA_RES_GROUP_PAD is padding attribute const NHA_RES_GROUP_PAD = NHA_RES_GROUP_UNSPEC // SupportedNlFamilies contains the list of netlink families this netlink package supports From 685a6940901cf90ad1488b4761c2cc47db58ef80 Mon Sep 17 00:00:00 2001 From: Arvind Sharma Date: Sat, 1 Aug 2026 23:53:37 +0530 Subject: [PATCH 3/8] fix test --- handle_linux.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/handle_linux.go b/handle_linux.go index a6e8346d2..6b0b6a6cf 100644 --- a/handle_linux.go +++ b/handle_linux.go @@ -83,6 +83,13 @@ func (h *Handle) DisableVFInfoCollection() *Handle { return h } +// RetryInterrupted configures the handle to automatically retry dump operations +// if they fail with EINTR before returning [ErrDumpInterrupted]. +func (h *Handle) RetryInterrupted() *Handle { + h.options.RetryInterrupted = true + return h +} + // SetSocketTimeout configures timeout for default netlink sockets func SetSocketTimeout(to time.Duration) error { if to < time.Microsecond { From 9049d48a69c3378a747bf9aa075217b3f627b749 Mon Sep 17 00:00:00 2001 From: astrobounce Date: Sun, 2 Aug 2026 13:57:25 +0530 Subject: [PATCH 4/8] added example for creating resilient-nexthop-group --- examples/resilient-nexthop-group/go.mod | 12 ++ examples/resilient-nexthop-group/go.sum | 12 ++ examples/resilient-nexthop-group/main.go | 170 +++++++++++++++++++++++ 3 files changed, 194 insertions(+) create mode 100644 examples/resilient-nexthop-group/go.mod create mode 100644 examples/resilient-nexthop-group/go.sum create mode 100644 examples/resilient-nexthop-group/main.go diff --git a/examples/resilient-nexthop-group/go.mod b/examples/resilient-nexthop-group/go.mod new file mode 100644 index 000000000..b5f59074c --- /dev/null +++ b/examples/resilient-nexthop-group/go.mod @@ -0,0 +1,12 @@ +module github.com/vishvananda/netlink/examples/resilient-nexthop-group + +go 1.26.3 + +require github.com/vishvananda/netlink v1.3.1 + +require ( + github.com/vishvananda/netns v0.0.5 // indirect + golang.org/x/sys v0.10.0 // indirect +) + +replace github.com/vishvananda/netlink => ../.. diff --git a/examples/resilient-nexthop-group/go.sum b/examples/resilient-nexthop-group/go.sum new file mode 100644 index 000000000..0e706645a --- /dev/null +++ b/examples/resilient-nexthop-group/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/resilient-nexthop-group/main.go b/examples/resilient-nexthop-group/main.go new file mode 100644 index 000000000..37b33e606 --- /dev/null +++ b/examples/resilient-nexthop-group/main.go @@ -0,0 +1,170 @@ +//go:build linux +// +build linux + +// Resilient nexhtop group creates a resilient group of nexthops +// The library provides the equivalent feature like commandline below: +// +// ip nexthop add id 1 via 10.0.0.1 dev eth0 +// ip nexthop add id 2 via 10.0.0.2 dev eth0 +// ip nexthop add id 100 group 1/2 type res buckets 8 idle_timer 60 unbalanced_time 300 + +// Usage +// sudo resilient-nexthop-group -dev eth0 -gw 10.0.0.1,10.0.0.2 +// sudo resilient-nexthop-group -dev eth0 -gw 10.0.0.1 -cleanup + +package main + +import ( + "flag" + "fmt" + "log" + "net" + "strconv" + "strings" + + "github.com/vishvananda/netlink" +) + +var ( + dev = flag.String("dev", "", "interface the members of nexthop points to") + gateways = flag.String("gw", "", "comma separated list of gateways for the nexthop group") + groupID = flag.Uint("group-id", 100, "nexthop group ID") + memberBaseID = flag.Uint("members-base-id", 1, "base ID for the members of the nexthop group") + buckets = flag.Uint("buckets", 8, "number of buckets for the resilient nexthop group") + idleTimer = flag.Uint("idle-timer", 60, "idle timer for the resilient nexthop group in seconds") + unbalancedTimer = flag.Uint("unbalanced-timer", 60, "unbalanced timer for the resilient nexthop group in seconds") + cleanup = flag.Bool("cleanup", false, "cleanup the nexthop group and its members") +) + +func main() { + log.SetFlags(0) + flag.Parse() + + if *dev == "" || *gateways == "" { + flag.Usage() + log.Fatal("device and gateways must be specified") + } + + link, err := netlink.LinkByName(*dev) + if err != nil { + log.Fatalf("failed to get link by name %s: %v", *dev, err) + } + + members, err := parseGateways(*gateways) + if err != nil { + log.Fatalf("failed to parse gateways: %v", err) + } + + if *groupID >= *memberBaseID && *groupID < *memberBaseID+uint(len(members)) { + log.Fatalf("group ID %d overlaps with member nexthop IDs [%d, %d); pick a distinct ID", + *groupID, *memberBaseID, *memberBaseID+uint(len(members))) + } + + if *cleanup { + cleanupNexthops(members) + return + } + + group := make([]netlink.NexthopGroupMpath, 0, len(members)) + + for i, m := range members { + id := uint32(*memberBaseID) + uint32(i) + nh := &netlink.Nexthop{ + ID: id, + OIF: uint32(link.Attrs().Index), + Gateway: m.gateway, + } + if err := netlink.NexthopReplace(nh); err != nil { + log.Fatalf("failed to add nexthop %v: %v", nh, err) + } + log.Printf("created member nexthop %d via %s: %v", id, m.gateway, *dev) + + group = append(group, netlink.NexthopGroupMpath{ + ID: id, + Weight: m.weight, + }) + } + + nhg := &netlink.Nexthop{ + ID: uint32(*groupID), + Group: group, + GroupType: netlink.NEXTHOP_GRP_TYPE_RES, + ResGroup: &netlink.NexthopResGroup{ + Buckets: uint16(*buckets), + IdleTimer: uint32(*idleTimer), + UnbalancedTimer: uint32(*unbalancedTimer), + }, + } + if err := netlink.NexthopReplace(nhg); err != nil { + log.Fatalf("failed to add resilient nexthop group: %v", err) + } + log.Println(describeGroup(nhg)) +} + +func cleanupNexthops(members []member) { + group := &netlink.Nexthop{ID: uint32(*groupID)} + if err := netlink.NexthopDel(group); err != nil { + log.Fatalf("failed to delete resilient nexthop group %d: %v", *groupID, err) + } + log.Printf("deleted resilient nexthop group %d", *groupID) + + for i := range members { + id := uint32(*memberBaseID) + uint32(i) + member := &netlink.Nexthop{ID: id} + if err := netlink.NexthopDel(member); err != nil { + log.Fatalf("failed to delete member nexthop %d: %v", id, err) + } + log.Printf("deleted member nexthop %d", id) + } +} + +type member struct { + gateway net.IP + weight uint16 +} + +func parseGateways(gateways string) ([]member, error) { + var members []member + for _, field := range strings.Split(gateways, ",") { + field = strings.TrimSpace(field) + if field == "" { + continue + } + + addr, weight := field, uint16(1) + if at := strings.Index(field, ":"); at != -1 { + addr = field[:at] + w, err := strconv.Atoi(field[at+1:]) + if err != nil { + return nil, fmt.Errorf("invalid weight for gateway %s: %v", field, err) + } + if w < 1 || w > 256 { + return nil, fmt.Errorf("weight for gateway %s must be between 1 and 256", field) + } + weight = uint16(w) + } + ip := net.ParseIP(addr) + if ip == nil { + return nil, fmt.Errorf("invalid gateway IP: %s", addr) + } + members = append(members, member{ + gateway: ip, + weight: weight, + }) + } + return members, nil +} + +func describeGroup(nh *netlink.Nexthop) string { + entries := make([]string, 0, len(nh.Group)) + for _, entry := range nh.Group { + entries = append(entries, fmt.Sprintf("%d", entry.ID)) + } + + desc := fmt.Sprintf("Nexthop group ID: %d, Members: [%s]", nh.ID, strings.Join(entries, ", ")) + if nh.ResGroup != nil { + desc += fmt.Sprintf(", Buckets: %d, IdleTimer: %d, UnbalancedTimer: %d, UnbalancedTime: %d", + nh.ResGroup.Buckets, nh.ResGroup.IdleTimer, nh.ResGroup.UnbalancedTimer, nh.ResGroup.UnbalancedTime) + } + return desc +} From dd78b43c35465523213801d186495bca30347345 Mon Sep 17 00:00:00 2001 From: astrobounce Date: Sun, 2 Aug 2026 22:49:39 +0530 Subject: [PATCH 5/8] added examples for nexthop group and resilient group and some nits --- CHANGELOG.md | 26 ++++ examples/nexthop-group/go.mod | 12 ++ examples/nexthop-group/go.sum | 12 ++ examples/nexthop-group/main.go | 160 +++++++++++++++++++++++ examples/nexthop/go.mod | 12 ++ examples/nexthop/go.sum | 12 ++ examples/nexthop/main.go | 128 ++++++++++++++++++ examples/resilient-nexthop-group/main.go | 14 +- nexthop.go | 26 ++-- nexthop_linux.go | 44 +++---- nexthop_test.go | 12 ++ 11 files changed, 416 insertions(+), 42 deletions(-) create mode 100644 examples/nexthop-group/go.mod create mode 100644 examples/nexthop-group/go.sum create mode 100644 examples/nexthop-group/main.go create mode 100644 examples/nexthop/go.mod create mode 100644 examples/nexthop/go.sum create mode 100644 examples/nexthop/main.go diff --git a/CHANGELOG.md b/CHANGELOG.md index b11e59ff6..a2701ac79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## Unreleased + +### nexthop: add support for Resilient Next-hop Groups + +**Background (what already existed upstream)** + +The upstream library previously had only *basic* nexthop support (from `Add a basic support for nexthop`): +- Low-level nexthop primitives in the `nl` package (`nl.Nhmsg`, `NewNexthopRequest`). +- `NexthopAdd` / `NexthopDel` / `NexthopList` / `NexthopReplace` APIs. +- Only single nexthops were supported, serializing `NHA_ID`, `NHA_BLACKHOLE`, `NHA_OIF`, `NHA_GATEWAY`, and `Protocol`. +- `NHID` field on `Route` to attach an existing nexthop object to a route. +- No notion of *groups*: `Nexthop` had no `Group`, `GroupType`, or `ResGroup` fields, so multipath and resilient nexthop groups (`ip nexthop ... group ... type res`) could not be created or parsed. + +**What this PR brings** + +- New API types: + - `NexthopGroupMpath{ID, Weight}` — a member (nexthop ID + relative weight, 1–256; 0 treated as 1). + - `NexthopResGroup{Buckets, IdleTimer, UnbalancedTimer, UnbalancedTime}` — resilient-group configuration. + - Group-type constants `NEXTHOP_GRP_TYPE_MPATH` and `NEXTHOP_GRP_TYPE_RES`. +- Extended `Nexthop` struct with `Group []NexthopGroupMpath`, `GroupType uint16`, and `ResGroup *NexthopResGroup`. +- Serialization/deserialization of the new attributes: `NHA_GROUP` (with weight encoding `wire = weight - 1`), `NHA_GROUP_TYPE`, and the nested `NHA_RES_GROUP` (`NHA_RES_GROUP_BUCKETS`, `NHA_RES_GROUP_IDLE_TIMER`, `NHA_RES_GROUP_UNBALANCED_TIMER`, `NHA_RES_GROUP_UNBALANCED_TIME`), where timers are converted to/from kernel `clock_t` units. +- New `nl` package constants: `NHA_RES_GROUP`, `NHA_RES_BUCKET`, `NHA_RES_GROUP_*`, `NHA_RES_GROUP_PAD`. +- Input validation in `prepareNewNexthop` (res-group requires `NEXTHOP_GRP_TYPE_RES`; non-empty group for a set group type) and family derivation (`FAMILY_ALL`) for group nexthops. +- `Handle.RetryInterrupted()` option to auto-retry interrupted dumps. +- New example `examples/resilient-nexthop-group` reproducing `ip nexthop add id group type res buckets idle_timer unbalanced_timer `. + ## 1.0.0 (2018-03-15) Initial release tagging \ No newline at end of file diff --git a/examples/nexthop-group/go.mod b/examples/nexthop-group/go.mod new file mode 100644 index 000000000..808793261 --- /dev/null +++ b/examples/nexthop-group/go.mod @@ -0,0 +1,12 @@ +module github.com/vishvananda/netlink/examples/nexthop-group + +go 1.26.3 + +require github.com/vishvananda/netlink v1.3.1 + +require ( + github.com/vishvananda/netns v0.0.5 // indirect + golang.org/x/sys v0.10.0 // indirect +) + +replace github.com/vishvananda/netlink => ../.. diff --git a/examples/nexthop-group/go.sum b/examples/nexthop-group/go.sum new file mode 100644 index 000000000..0e706645a --- /dev/null +++ b/examples/nexthop-group/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/nexthop-group/main.go b/examples/nexthop-group/main.go new file mode 100644 index 000000000..4374f5027 --- /dev/null +++ b/examples/nexthop-group/main.go @@ -0,0 +1,160 @@ +//go:build linux +// +build linux + +// Multipath nexthop group creates a nexthop group without the resilient +// (NEXTHOP_GRP_TYPE_RES) support. +// The library provides the equivalent feature like commandline below: +// +// ip nexthop add id 1 via 10.0.0.1 dev eth0 +// ip nexthop add id 2 via 10.0.0.2 dev eth0 +// ip nexthop add id 100 group 1/2 +// +// A group created without an explicit group type is a multi-path (mpath) +// nexthop group. Weight of each member can be specified with the ":" suffix, +// e.g. 10.0.0.1:2 for a weight of 2. + +// Usage +// sudo nexthop-group -dev eth0 -gw 10.0.0.1,10.0.0.2 +// sudo nexthop-group -dev eth0 -gw 10.0.0.1 -cleanup + +package main + +import ( + "flag" + "fmt" + "log" + "net" + "strconv" + "strings" + + "github.com/vishvananda/netlink" +) + +var ( + dev = flag.String("dev", "", "interface the members of nexthop group points to") + gateways = flag.String("gw", "", "comma separated list of gateways for the nexthop group") + groupID = flag.Uint("group-id", 100, "nexthop group ID") + memberBaseID = flag.Uint("members-base-id", 1, "base ID for the members of the nexthop group") + cleanup = flag.Bool("cleanup", false, "cleanup the nexthop group and its members") +) + +func main() { + log.SetFlags(0) + flag.Parse() + + if *dev == "" || *gateways == "" { + flag.Usage() + log.Fatal("device and gateways must be specified") + } + + link, err := netlink.LinkByName(*dev) + if err != nil { + log.Fatalf("failed to get link by name %s: %v", *dev, err) + } + + members, err := parseGateways(*gateways) + if err != nil { + log.Fatalf("failed to parse gateways: %v", err) + } + + if *groupID >= *memberBaseID && *groupID < *memberBaseID+uint(len(members)) { + log.Fatalf("group ID %d overlaps with member nexthop IDs [%d, %d); pick a distinct ID", + *groupID, *memberBaseID, *memberBaseID+uint(len(members))) + } + + if *cleanup { + cleanupNexthops(members) + return + } + + group := make([]netlink.NexthopGroupMember, 0, len(members)) + + for i, m := range members { + id := uint32(*memberBaseID) + uint32(i) + nh := &netlink.Nexthop{ + ID: id, + OIF: uint32(link.Attrs().Index), + Gateway: m.gateway, + } + if err := netlink.NexthopReplace(nh); err != nil { + log.Fatalf("failed to add nexthop %v: %v", nh, err) + } + log.Printf("created member nexthop %d via %s: %v", id, m.gateway, *dev) + + group = append(group, netlink.NexthopGroupMember{ + ID: id, + Weight: m.weight, + }) + } + + nhg := &netlink.Nexthop{ + ID: uint32(*groupID), + Group: group, + } + if err := netlink.NexthopReplace(nhg); err != nil { + log.Fatalf("failed to add multipath nexthop group: %v", err) + } + log.Println(describeGroup(nhg)) +} + +func cleanupNexthops(members []member) { + group := &netlink.Nexthop{ID: uint32(*groupID)} + if err := netlink.NexthopDel(group); err != nil { + log.Fatalf("failed to delete multipath nexthop group %d: %v", *groupID, err) + } + log.Printf("deleted multipath nexthop group %d", *groupID) + + for i := range members { + id := uint32(*memberBaseID) + uint32(i) + member := &netlink.Nexthop{ID: id} + if err := netlink.NexthopDel(member); err != nil { + log.Fatalf("failed to delete member nexthop %d: %v", id, err) + } + log.Printf("deleted member nexthop %d", id) + } +} + +type member struct { + gateway net.IP + weight uint16 +} + +func parseGateways(gateways string) ([]member, error) { + var members []member + for _, field := range strings.Split(gateways, ",") { + field = strings.TrimSpace(field) + if field == "" { + continue + } + + addr, weight := field, uint16(1) + if at := strings.Index(field, ":"); at != -1 { + addr = field[:at] + w, err := strconv.Atoi(field[at+1:]) + if err != nil { + return nil, fmt.Errorf("invalid weight for gateway %s: %v", field, err) + } + if w < 1 || w > 256 { + return nil, fmt.Errorf("weight for gateway %s must be between 1 and 256", field) + } + weight = uint16(w) + } + ip := net.ParseIP(addr) + if ip == nil { + return nil, fmt.Errorf("invalid gateway IP: %s", addr) + } + members = append(members, member{ + gateway: ip, + weight: weight, + }) + } + return members, nil +} + +func describeGroup(nh *netlink.Nexthop) string { + entries := make([]string, 0, len(nh.Group)) + for _, entry := range nh.Group { + entries = append(entries, fmt.Sprintf("%d", entry.ID)) + } + return fmt.Sprintf("Nexthop group ID: %d, Type: mpath, Members: [%s]", nh.ID, strings.Join(entries, ", ")) +} diff --git a/examples/nexthop/go.mod b/examples/nexthop/go.mod new file mode 100644 index 000000000..9f0e8ae21 --- /dev/null +++ b/examples/nexthop/go.mod @@ -0,0 +1,12 @@ +module github.com/vishvananda/netlink/examples/nexthop + +go 1.26.3 + +require github.com/vishvananda/netlink v1.3.1 + +require ( + github.com/vishvananda/netns v0.0.5 // indirect + golang.org/x/sys v0.10.0 // indirect +) + +replace github.com/vishvananda/netlink => ../.. diff --git a/examples/nexthop/go.sum b/examples/nexthop/go.sum new file mode 100644 index 000000000..0e706645a --- /dev/null +++ b/examples/nexthop/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/nexthop/main.go b/examples/nexthop/main.go new file mode 100644 index 000000000..414e651d2 --- /dev/null +++ b/examples/nexthop/main.go @@ -0,0 +1,128 @@ +//go:build linux +// +build linux + +// Basic nexthop creates a nexthop object +// The library provides the equivalent feature like commandline below: +// +// ip nexthop add id 1 via 10.0.0.1 dev eth0 +// ip nexthop del id 1 +// ip nexthop show + +// Usage +// sudo nexthop -dev eth0 -gw 10.0.0.1 +// sudo nexthop -dev eth0 -gw 10.0.0.1 -id 2 +// sudo nexthop -dev eth0 -gw 10.0.0.1 -cleanup + +package main + +import ( + "flag" + "fmt" + "log" + "net" + "strconv" + "strings" + + "github.com/vishvananda/netlink" +) + +var ( + dev = flag.String("dev", "", "interface the nexthop points to") + gateway = flag.String("gw", "", "gateway IP for the nexthop") + id = flag.Uint("id", 1, "nexthop ID") + blackhole = flag.Bool("blackhole", false, "create a blackhole nexthop") + cleanup = flag.Bool("cleanup", false, "cleanup the nexthop") +) + +func main() { + log.SetFlags(0) + flag.Parse() + + if *dev == "" { + flag.Usage() + log.Fatal("device must be specified") + } + + link, err := netlink.LinkByName(*dev) + if err != nil { + log.Fatalf("failed to get link by name %s: %v", *dev, err) + } + + nh, err := buildNexthop(link) + if err != nil { + log.Fatalf("failed to build nexthop: %v", err) + } + + if *cleanup { + cleanupNexthop(nh) + return + } + + if err := netlink.NexthopReplace(nh); err != nil { + log.Fatalf("failed to add nexthop %v: %v", nh, err) + } + log.Printf("created nexthop %d via %s dev %s", nh.ID, nh.Gateway, *dev) + + listNexthops() +} + +func buildNexthop(link netlink.Link) (*netlink.Nexthop, error) { + nh := &netlink.Nexthop{ + ID: uint32(*id), + OIF: uint32(link.Attrs().Index), + } + + if *blackhole { + nh.Blackhole = true + return nh, nil + } + + if *gateway == "" { + return nil, fmt.Errorf("gateway (-gw) is required unless -blackhole is set") + } + ip := net.ParseIP(*gateway) + if ip == nil { + return nil, fmt.Errorf("invalid gateway IP: %s", *gateway) + } + nh.Gateway = ip + return nh, nil +} + +func cleanupNexthop(nh *netlink.Nexthop) { + del := &netlink.Nexthop{ID: nh.ID} + if err := netlink.NexthopDel(del); err != nil { + log.Fatalf("failed to delete nexthop %d: %v", nh.ID, err) + } + log.Printf("deleted nexthop %d", nh.ID) +} + +func listNexthops() { + nhs, err := netlink.NexthopList() + if err != nil { + log.Fatalf("failed to list nexthops: %v", err) + } + if len(nhs) == 0 { + log.Println("no nexthops present") + return + } + rows := make([]string, 0, len(nhs)) + for _, nh := range nhs { + rows = append(rows, describeNexthop(&nh)) + } + log.Printf("nexthops:\n%s", strings.Join(rows, "\n")) +} + +func describeNexthop(nh *netlink.Nexthop) string { + var parts []string + if nh.Blackhole { + parts = append(parts, "blackhole") + } + if nh.OIF > 0 { + parts = append(parts, "dev "+strconv.FormatUint(uint64(nh.OIF), 10)) + } + if nh.Gateway != nil { + parts = append(parts, "via "+nh.Gateway.String()) + } + parts = append(parts, "protocol "+nh.Protocol.String()) + return fmt.Sprintf("id %d %s", nh.ID, strings.Join(parts, " ")) +} diff --git a/examples/resilient-nexthop-group/main.go b/examples/resilient-nexthop-group/main.go index 37b33e606..6e229ccab 100644 --- a/examples/resilient-nexthop-group/main.go +++ b/examples/resilient-nexthop-group/main.go @@ -1,12 +1,12 @@ //go:build linux // +build linux -// Resilient nexhtop group creates a resilient group of nexthops +// Resilient nexthop group creates a resilient group of nexthops. // The library provides the equivalent feature like commandline below: // // ip nexthop add id 1 via 10.0.0.1 dev eth0 // ip nexthop add id 2 via 10.0.0.2 dev eth0 -// ip nexthop add id 100 group 1/2 type res buckets 8 idle_timer 60 unbalanced_time 300 +// ip nexthop add id 100 group 1/2 type res buckets 8 idle_timer 60 unbalanced_timer 300 // Usage // sudo resilient-nexthop-group -dev eth0 -gw 10.0.0.1,10.0.0.2 @@ -65,7 +65,7 @@ func main() { return } - group := make([]netlink.NexthopGroupMpath, 0, len(members)) + group := make([]netlink.NexthopGroupMember, 0, len(members)) for i, m := range members { id := uint32(*memberBaseID) + uint32(i) @@ -79,7 +79,7 @@ func main() { } log.Printf("created member nexthop %d via %s: %v", id, m.gateway, *dev) - group = append(group, netlink.NexthopGroupMpath{ + group = append(group, netlink.NexthopGroupMember{ ID: id, Weight: m.weight, }) @@ -89,7 +89,7 @@ func main() { ID: uint32(*groupID), Group: group, GroupType: netlink.NEXTHOP_GRP_TYPE_RES, - ResGroup: &netlink.NexthopResGroup{ + ResilientGroup: &netlink.NexthopResilientGroup{ Buckets: uint16(*buckets), IdleTimer: uint32(*idleTimer), UnbalancedTimer: uint32(*unbalancedTimer), @@ -162,9 +162,9 @@ func describeGroup(nh *netlink.Nexthop) string { } desc := fmt.Sprintf("Nexthop group ID: %d, Members: [%s]", nh.ID, strings.Join(entries, ", ")) - if nh.ResGroup != nil { + if nh.ResilientGroup != nil { desc += fmt.Sprintf(", Buckets: %d, IdleTimer: %d, UnbalancedTimer: %d, UnbalancedTime: %d", - nh.ResGroup.Buckets, nh.ResGroup.IdleTimer, nh.ResGroup.UnbalancedTimer, nh.ResGroup.UnbalancedTime) + nh.ResilientGroup.Buckets, nh.ResilientGroup.IdleTimer, nh.ResilientGroup.UnbalancedTimer, nh.ResilientGroup.UnbalancedTime) } return desc } diff --git a/nexthop.go b/nexthop.go index 184da8100..d54f16e55 100644 --- a/nexthop.go +++ b/nexthop.go @@ -9,40 +9,40 @@ import ( // Nexthop group types - reference https://github.com/torvalds/linux/blob/master/include/uapi/linux/nexthop.h const ( - // NEXTHOP_GRP_TYPE_MPATH is default multi-path hash threshold + // NEXTHOP_GRP_TYPE_MPATH is the default multi-path hash threshold group type. NEXTHOP_GRP_TYPE_MPATH uint16 = iota - // NEXTHOP_GRP_TYPE_RES is Resilient nexthop group + // NEXTHOP_GRP_TYPE_RES is a resilient nexthop group. NEXTHOP_GRP_TYPE_RES ) -// NexthopGroupMpath represents one member of a nexthtop group -type NexthopGroupMpath struct { - // ID of an existing nexthop to include in the group +// NexthopGroupMember represents one member of a nexthop group. +type NexthopGroupMember struct { + // ID of an existing nexthop to include in the group. ID uint32 - // Relative weight, 1-256. Zero is treated as 1 + // Relative weight, 1-256. Zero is treated as 1. Weight uint16 } -// NexthopResGroup is resilient nexthop group structure -type NexthopResGroup struct { +// NexthopResilientGroup contains the configuration for a resilient nexthop group. +type NexthopResilientGroup struct { Buckets uint16 IdleTimer uint32 UnbalancedTimer uint32 UnbalancedTime uint64 } -// Nexthop represent a nexthop object +// Nexthop represents a nexthop object. type Nexthop struct { ID uint32 Blackhole bool OIF uint32 Gateway net.IP Protocol RouteProtocol - // Nexthop group members for resilient nexthop group or multipath nexthop group - Group []NexthopGroupMpath - GroupType uint16 - ResGroup *NexthopResGroup + // Group holds nexthop group members for multipath or resilient groups. + Group []NexthopGroupMember + GroupType uint16 + ResilientGroup *NexthopResilientGroup } func (h *Nexthop) String() string { diff --git a/nexthop_linux.go b/nexthop_linux.go index d0496ac2b..c34dc3b87 100644 --- a/nexthop_linux.go +++ b/nexthop_linux.go @@ -10,11 +10,11 @@ import ( ) const ( - // sizeofNexthopGrp is size of single nexhthop group member - sizeofNexthopGrp = 8 + // sizeofNexthopGroupMember is the size of a single nexthop group member. + sizeofNexthopGroupMember = 8 - // nexthopResGroupUserHZ is the userspace clock ticks per second - nexthopResGroupUserHZ = 100 + // nexthopResilientGroupUserHz is the userspace clock ticks per second. + nexthopResilientGroupUserHz = 100 ) // NexthopAdd will add a nexthop to the system. @@ -173,7 +173,7 @@ var nexthopAttrHandlers = map[uint16]struct { if len(nh.Group) == 0 { return nil } - b := make([]byte, sizeofNexthopGrp*len(nh.Group)) + b := make([]byte, sizeofNexthopGroupMember*len(nh.Group)) for i, entry := range nh.Group { // Kernel interprets one weight wire = actual weight - 1 w := entry.Weight @@ -182,7 +182,7 @@ var nexthopAttrHandlers = map[uint16]struct { } else if w > 256 { w = 256 } - off := i * sizeofNexthopGrp + off := i * sizeofNexthopGroupMember native.PutUint32(b[off:off+4], entry.ID) b[off+4] = uint8(w - 1) } @@ -190,8 +190,8 @@ var nexthopAttrHandlers = map[uint16]struct { }, decode: func(nh *Nexthop, attr *nl.RtAttr) { nh.Group = nil - for off := 0; off+sizeofNexthopGrp <= len(attr.Data); off += sizeofNexthopGrp { - nh.Group = append(nh.Group, NexthopGroupMpath{ + for off := 0; off+sizeofNexthopGroupMember <= len(attr.Data); off += sizeofNexthopGroupMember { + nh.Group = append(nh.Group, NexthopGroupMember{ ID: native.Uint32(attr.Data[off : off+4]), Weight: uint16(attr.Data[off+4]) + 1, }) @@ -216,25 +216,25 @@ var nexthopAttrHandlers = map[uint16]struct { }, nl.NHA_RES_GROUP: { encode: func(nh *Nexthop) *nl.RtAttr { - if nh.ResGroup == nil { + if nh.ResilientGroup == nil { return nil } // Strict netlink validation requires the NLA_F_NESTED // flag on nested attributes. attr := nl.NewRtAttr(nl.NHA_RES_GROUP|int(nl.NLA_F_NESTED), nil) - if nh.ResGroup.Buckets > 0 { + if nh.ResilientGroup.Buckets > 0 { b := make([]byte, 2) - native.PutUint16(b, nh.ResGroup.Buckets) + native.PutUint16(b, nh.ResilientGroup.Buckets) attr.AddRtAttr(nl.NHA_RES_GROUP_BUCKETS, b) } - if nh.ResGroup.IdleTimer > 0 { + if nh.ResilientGroup.IdleTimer > 0 { b := make([]byte, 4) - native.PutUint32(b, nh.ResGroup.IdleTimer*nexthopResGroupUserHZ) + native.PutUint32(b, nh.ResilientGroup.IdleTimer*nexthopResilientGroupUserHz) attr.AddRtAttr(nl.NHA_RES_GROUP_IDLE_TIMER, b) } - if nh.ResGroup.UnbalancedTimer > 0 { + if nh.ResilientGroup.UnbalancedTimer > 0 { b := make([]byte, 4) - native.PutUint32(b, nh.ResGroup.UnbalancedTimer*nexthopResGroupUserHZ) + native.PutUint32(b, nh.ResilientGroup.UnbalancedTimer*nexthopResilientGroupUserHz) attr.AddRtAttr(nl.NHA_RES_GROUP_UNBALANCED_TIMER, b) } return attr @@ -244,7 +244,7 @@ var nexthopAttrHandlers = map[uint16]struct { if err != nil { return } - res := &NexthopResGroup{} + res := &NexthopResilientGroup{} for _, a := range nested { switch a.Attr.Type & nl.NLA_TYPE_MASK { case nl.NHA_RES_GROUP_BUCKETS: @@ -253,19 +253,19 @@ var nexthopAttrHandlers = map[uint16]struct { } case nl.NHA_RES_GROUP_IDLE_TIMER: if len(a.Value) >= 4 { - res.IdleTimer = native.Uint32(a.Value[0:4]) / nexthopResGroupUserHZ + res.IdleTimer = native.Uint32(a.Value[0:4]) / nexthopResilientGroupUserHz } case nl.NHA_RES_GROUP_UNBALANCED_TIMER: if len(a.Value) >= 4 { - res.UnbalancedTimer = native.Uint32(a.Value[0:4]) / nexthopResGroupUserHZ + res.UnbalancedTimer = native.Uint32(a.Value[0:4]) / nexthopResilientGroupUserHz } case nl.NHA_RES_GROUP_UNBALANCED_TIME: if len(a.Value) >= 8 { - res.UnbalancedTime = native.Uint64(a.Value[0:8]) / nexthopResGroupUserHZ + res.UnbalancedTime = native.Uint64(a.Value[0:8]) / nexthopResilientGroupUserHz } } } - nh.ResGroup = res + nh.ResilientGroup = res }, }, unix.NHA_GATEWAY: { @@ -352,8 +352,8 @@ func deriveFamilyFromNexthop(nh *Nexthop) uint8 { } func prepareNewNexthop(nh *Nexthop, req *nl.NetlinkRequest, msg *nl.Nhmsg) error { - if nh.ResGroup != nil && nh.GroupType != NEXTHOP_GRP_TYPE_RES { - return fmt.Errorf("nexthop: ResGroup requires GroupType to be NEXTHOP_GRP_TYPE_RES") + if nh.ResilientGroup != nil && nh.GroupType != NEXTHOP_GRP_TYPE_RES { + return fmt.Errorf("nexthop: ResilientGroup requires GroupType to be NEXTHOP_GRP_TYPE_RES") } if nh.GroupType != NEXTHOP_GRP_TYPE_MPATH && len(nh.Group) == 0 { return fmt.Errorf("nexthop: GroupType is set but Group is empty") diff --git a/nexthop_test.go b/nexthop_test.go index ebe1ae80e..51834ccbb 100644 --- a/nexthop_test.go +++ b/nexthop_test.go @@ -11,6 +11,18 @@ import ( "golang.org/x/sys/unix" ) +func TestNexthopGroupMemberAndResilientGroupTypes(t *testing.T) { + member := NexthopGroupMember{ID: 1, Weight: 3} + group := &NexthopResilientGroupAttrs{Buckets: 8, IdleTimer: 30} + + if member.ID != 1 || member.Weight != 3 { + t.Fatalf("unexpected member values: %+v", member) + } + if group.Buckets != 8 || group.IdleTimer != 30 { + t.Fatalf("unexpected resilient group values: %+v", group) + } +} + func TestNexthopAddListDelReplace(t *testing.T) { t.Cleanup(setUpNetlinkTest(t)) From 2b9882338bebd567199d92b6013040367d4d223b Mon Sep 17 00:00:00 2001 From: astrobounce Date: Sun, 2 Aug 2026 23:00:29 +0530 Subject: [PATCH 6/8] Fix test issue --- nexthop_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nexthop_test.go b/nexthop_test.go index 51834ccbb..2ab405584 100644 --- a/nexthop_test.go +++ b/nexthop_test.go @@ -13,7 +13,7 @@ import ( func TestNexthopGroupMemberAndResilientGroupTypes(t *testing.T) { member := NexthopGroupMember{ID: 1, Weight: 3} - group := &NexthopResilientGroupAttrs{Buckets: 8, IdleTimer: 30} + group := &NexthopResilientGroup{Buckets: 8, IdleTimer: 30} if member.ID != 1 || member.Weight != 3 { t.Fatalf("unexpected member values: %+v", member) From 3df0819ca0e94731d426488864c639691227467f Mon Sep 17 00:00:00 2001 From: Arvind Sharma Date: Sun, 2 Aug 2026 23:09:51 +0530 Subject: [PATCH 7/8] Remove changes from CHANGELOG.md Removed unreleased section detailing nexthop support for resilient next-hop groups, including background and new API types. --- CHANGELOG.md | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2701ac79..e1ef03cb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,31 +1,5 @@ # Changelog -## Unreleased - -### nexthop: add support for Resilient Next-hop Groups - -**Background (what already existed upstream)** - -The upstream library previously had only *basic* nexthop support (from `Add a basic support for nexthop`): -- Low-level nexthop primitives in the `nl` package (`nl.Nhmsg`, `NewNexthopRequest`). -- `NexthopAdd` / `NexthopDel` / `NexthopList` / `NexthopReplace` APIs. -- Only single nexthops were supported, serializing `NHA_ID`, `NHA_BLACKHOLE`, `NHA_OIF`, `NHA_GATEWAY`, and `Protocol`. -- `NHID` field on `Route` to attach an existing nexthop object to a route. -- No notion of *groups*: `Nexthop` had no `Group`, `GroupType`, or `ResGroup` fields, so multipath and resilient nexthop groups (`ip nexthop ... group ... type res`) could not be created or parsed. - -**What this PR brings** - -- New API types: - - `NexthopGroupMpath{ID, Weight}` — a member (nexthop ID + relative weight, 1–256; 0 treated as 1). - - `NexthopResGroup{Buckets, IdleTimer, UnbalancedTimer, UnbalancedTime}` — resilient-group configuration. - - Group-type constants `NEXTHOP_GRP_TYPE_MPATH` and `NEXTHOP_GRP_TYPE_RES`. -- Extended `Nexthop` struct with `Group []NexthopGroupMpath`, `GroupType uint16`, and `ResGroup *NexthopResGroup`. -- Serialization/deserialization of the new attributes: `NHA_GROUP` (with weight encoding `wire = weight - 1`), `NHA_GROUP_TYPE`, and the nested `NHA_RES_GROUP` (`NHA_RES_GROUP_BUCKETS`, `NHA_RES_GROUP_IDLE_TIMER`, `NHA_RES_GROUP_UNBALANCED_TIMER`, `NHA_RES_GROUP_UNBALANCED_TIME`), where timers are converted to/from kernel `clock_t` units. -- New `nl` package constants: `NHA_RES_GROUP`, `NHA_RES_BUCKET`, `NHA_RES_GROUP_*`, `NHA_RES_GROUP_PAD`. -- Input validation in `prepareNewNexthop` (res-group requires `NEXTHOP_GRP_TYPE_RES`; non-empty group for a set group type) and family derivation (`FAMILY_ALL`) for group nexthops. -- `Handle.RetryInterrupted()` option to auto-retry interrupted dumps. -- New example `examples/resilient-nexthop-group` reproducing `ip nexthop add id group type res buckets idle_timer unbalanced_timer `. - ## 1.0.0 (2018-03-15) -Initial release tagging \ No newline at end of file +Initial release tagging From 44a841597f6d4b732334754fef47488a69872a0d Mon Sep 17 00:00:00 2001 From: astrobounce Date: Sun, 2 Aug 2026 23:13:16 +0530 Subject: [PATCH 8/8] Add recommendation by coderabbit --- examples/nexthop/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/nexthop/main.go b/examples/nexthop/main.go index 414e651d2..fb206feca 100644 --- a/examples/nexthop/main.go +++ b/examples/nexthop/main.go @@ -68,8 +68,7 @@ func main() { func buildNexthop(link netlink.Link) (*netlink.Nexthop, error) { nh := &netlink.Nexthop{ - ID: uint32(*id), - OIF: uint32(link.Attrs().Index), + ID: uint32(*id), } if *blackhole { @@ -77,6 +76,7 @@ func buildNexthop(link netlink.Link) (*netlink.Nexthop, error) { return nh, nil } + nh.OIF = uint32(link.Attrs().Index) if *gateway == "" { return nil, fmt.Errorf("gateway (-gw) is required unless -blackhole is set") }