Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`.
Comment thread
astrobounce marked this conversation as resolved.
Outdated
- 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 <N> group <members> type res buckets <n> idle_timer <s> unbalanced_timer <s>`.

## 1.0.0 (2018-03-15)

Initial release tagging
12 changes: 12 additions & 0 deletions examples/nexthop-group/go.mod
Original file line number Diff line number Diff line change
@@ -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 => ../..
12 changes: 12 additions & 0 deletions examples/nexthop-group/go.sum
Original file line number Diff line number Diff line change
@@ -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=
160 changes: 160 additions & 0 deletions examples/nexthop-group/main.go
Original file line number Diff line number Diff line change
@@ -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, ", "))
}
12 changes: 12 additions & 0 deletions examples/nexthop/go.mod
Original file line number Diff line number Diff line change
@@ -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 => ../..
12 changes: 12 additions & 0 deletions examples/nexthop/go.sum
Original file line number Diff line number Diff line change
@@ -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=
128 changes: 128 additions & 0 deletions examples/nexthop/main.go
Original file line number Diff line number Diff line change
@@ -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
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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, " "))
}
12 changes: 12 additions & 0 deletions examples/resilient-nexthop-group/go.mod
Original file line number Diff line number Diff line change
@@ -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 => ../..
Loading
Loading