Skip to content

Commit

Permalink
Add LinkDriver interface and Driver package (#221)
Browse files Browse the repository at this point in the history
* Add attr validation

This commit removes unix.IFLA_UNSPEC and introduces checks for the interface name,
the link type and the queue disc fields.
Interface name validation is necessary to prevent an 'invalid argument' error
when creating a link with an empty name. Other checks were added to be consistent with the ip tools.

Signed-off-by: Birol Bilgin <birolbilgin@gmail.com>

* Add network namespace type

This commit introduces the NetNS struct and integrates network namespace capabilities into link attributes.
This enhancement facilitates the creation of links, such as veth pairs, across different network namespaces
without the need to execute directly within those namespaces.

Signed-off-by: Birol Bilgin <birolbilgin@gmail.com>

* Add LinkDriver interface

This commit introduces a Driver interface and changes Data and SlaveData fields within the LinkInfo struct
as LinkDriver to accommodate driver-specific data encoding, addressing the limitation
where LinkInfo.Data and SlaveData fields were merely byte slices without support for specific data encoding.

Drivers are registered globally with the RegisterDriver function.
For un-registered drivers, the default LinkData driver is used.

Signed-off-by: Birol Bilgin <birolbilgin@gmail.com>

* Add Driver Package

This commit introduces the 'driver' package, which contains specific implementations of the LinkDriver interface.
It also includes the implementation of the Linux bond driver as LinkDriver and the bond slave driver as LinkSlaveDriver.

Signed-off-by: Birol Bilgin <birolbilgin@gmail.com>

* Add Netkit and Veth drivers

This commit adds Netkit and Veth drivers.

Signed-off-by: Birol Bilgin <birolbilgin@gmail.com>

---------

Signed-off-by: Birol Bilgin <birolbilgin@gmail.com>
  • Loading branch information
brlbil authored May 10, 2024
1 parent 1229f67 commit bd79d59
Show file tree
Hide file tree
Showing 13 changed files with 2,209 additions and 309 deletions.
811 changes: 811 additions & 0 deletions driver/bond.go

Large diffs are not rendered by default.

216 changes: 216 additions & 0 deletions driver/bond_live_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
//go:build integration
// +build integration

package driver

import (
"fmt"
"net"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/jsimonetti/rtnetlink"
"github.com/mdlayher/netlink"
)

func bondT(d rtnetlink.LinkDriver) *Bond {
b := d.(*Bond)
return &Bond{
Mode: b.Mode,
Miimon: b.Miimon,
ArpInterval: b.ArpInterval,
ArpIpTargets: b.ArpIpTargets,
NsIP6Targets: b.NsIP6Targets,
}
}

func bondSlaveT(d rtnetlink.LinkDriver) *BondSlave {
b := d.(*BondSlave)
return &BondSlave{
State: b.State,
MiiStatus: b.MiiStatus,
Priority: b.Priority,
}
}

func TestBond(t *testing.T) {
// establish a netlink connection
conn, err := rtnetlink.Dial(nil)
if err != nil {
t.Fatalf("failed to establish netlink socket: %v", err)
}
defer conn.Close()

bns, clean, err := createNS("bns1")
if err != nil {
t.Fatal(err)
}
defer clean()

// use ns for testing arp ip targets
connNS, err := rtnetlink.Dial(&netlink.Config{NetNS: int(bns.Value())})
if err != nil {
t.Fatalf("failed to establish netlink socket to ns nkns: %v", err)
}
defer connNS.Close()

var (
ssa = BondStateActive
ssb = BondStateBackup
miiup = BondLinkUp
u325 uint32 = 5
u32100 uint32 = 100
u32 uint32
i321 int32 = 1
i32 int32
)

tests := []struct {
name string
conn *rtnetlink.Conn
driver *Bond
bond *Bond
setSlave bool
dummy []BondSlave
}{
{
name: "with default mode and miion is set",
conn: conn,
driver: &Bond{
Miimon: &u32100,
},
bond: &Bond{
Mode: BondModeBalanceRR,
Miimon: &u32100,
ArpInterval: &u32,
},
dummy: []BondSlave{
{
State: &ssa,
MiiStatus: &miiup,
Priority: &i32,
},
{
State: &ssa,
MiiStatus: &miiup,
Priority: &i32,
},
},
},
{
name: "with active backup, and arp ip targets list",
conn: connNS,
driver: &Bond{
Mode: BondModeActiveBackup,
ArpInterval: &u325,
ArpIpTargets: []net.IP{{192, 168, 222, 2}, {192, 168, 222, 3}},
},
bond: &Bond{
Mode: BondModeActiveBackup,
Miimon: &u32,
ArpInterval: &u325,
ArpIpTargets: []net.IP{{192, 168, 222, 2}, {192, 168, 222, 3}},
},
setSlave: true,
dummy: []BondSlave{
{
State: &ssb,
MiiStatus: &miiup,
Priority: &i32,
},
{
State: &ssa,
MiiStatus: &miiup,
Priority: &i321,
},
},
},
{
name: "with balanced xor, and arp ns ipv6 list",
conn: connNS,
driver: &Bond{
Mode: BondModeBalanceXOR,
ArpInterval: &u325,
NsIP6Targets: []net.IP{
{0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02},
{0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03},
},
},
bond: &Bond{
Mode: BondModeBalanceXOR,
Miimon: &u32,
ArpInterval: &u325,
NsIP6Targets: []net.IP{
{0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02},
{0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03},
},
},
dummy: []BondSlave{
{
State: &ssa,
MiiStatus: &miiup,
Priority: &i32,
},
{
State: &ssa,
MiiStatus: &miiup,
Priority: &i32,
},
},
},
}

for i, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bondID := 1100 + uint32(i*10)
if err := setupInterface(tt.conn, fmt.Sprintf("b%d", bondID), bondID, 0, tt.driver); err != nil {
t.Fatalf("failed to setup bond interface: %v", err)
}
defer tt.conn.Link.Delete(bondID)

msg, err := getInterface(tt.conn, bondID)
if err != nil {
t.Fatalf("failed to get primary netkit interface: %v", err)
}
if diff := cmp.Diff(tt.bond, bondT(msg.Attributes.Info.Data)); diff != "" {
t.Error(diff)
}

slave1ID := 1101 + uint32(i*10)
if err := setupInterface(tt.conn, fmt.Sprintf("d%d", slave1ID), slave1ID, bondID, &rtnetlink.LinkData{Name: "dummy"}); err != nil {
t.Fatalf("failed to setup d%d interface: %v", slave1ID, err)
}
defer tt.conn.Link.Delete(slave1ID)

slave2ID := 1102 + uint32(i*10)
if err := setupInterface(tt.conn, fmt.Sprintf("d%d", slave2ID), slave2ID, bondID, &rtnetlink.LinkData{Name: "dummy"}); err != nil {
t.Fatalf("failed to setup d1%d interface: %v", slave2ID, err)
}
defer tt.conn.Link.Delete(slave2ID)

if tt.setSlave {
tt.conn.Link.Set(&rtnetlink.LinkMessage{
Index: slave2ID,
Attributes: &rtnetlink.LinkAttributes{
Info: &rtnetlink.LinkInfo{
SlaveKind: "bond",
SlaveData: &BondSlave{
Priority: &i321,
},
},
},
})
}

for i, id := range []uint32{slave1ID, slave2ID} {
msg, err = getInterface(tt.conn, id)
if err != nil {
t.Fatalf("failed to get peer netkit interface: %v", err)
}
if diff := cmp.Diff(&tt.dummy[i], bondSlaveT(msg.Attributes.Info.SlaveData)); diff != "" {
t.Errorf("slave %d %s", i, diff)
}
}
})
}
}
21 changes: 21 additions & 0 deletions driver/driver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package driver

import (
"github.com/jsimonetti/rtnetlink"
)

// init registers predefined drivers with the rtnetlink package.
//
// Currently, registering driver implementations that conflict with existing ones isn't supported.
// Since most users don't need this feature, we'll keep it as is.
// If required, we could consider implementing rtnetlink.UnregisterDriver to address this.
func init() {
for _, drv := range []rtnetlink.LinkDriver{
&Bond{},
&BondSlave{},
&Netkit{},
&Veth{},
} {
_ = rtnetlink.RegisterDriver(drv)
}
}
103 changes: 103 additions & 0 deletions driver/driver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//go:build integration
// +build integration

package driver

import (
"bytes"
"fmt"
"os/exec"
"testing"

"github.com/jsimonetti/rtnetlink"
"golang.org/x/sys/unix"
)

func getKernelVersion() (kernel, major, minor int, err error) {
var uname unix.Utsname
if err := unix.Uname(&uname); err != nil {
return 0, 0, 0, err
}

end := bytes.IndexByte(uname.Release[:], 0)
versionStr := uname.Release[:end]

if count, _ := fmt.Sscanf(string(versionStr), "%d.%d.%d", &kernel, &major, &minor); count < 2 {
err = fmt.Errorf("failed to parse kernel version from: %q", string(versionStr))
}
return
}

// kernelMinReq checks if the runtime kernel is sufficient
// for the test
func kernelMinReq(t *testing.T, kernel, major int) {
k, m, _, err := getKernelVersion()
if err != nil {
t.Fatalf("failed to get host kernel version: %v", err)
}
if k < kernel || k == kernel && m < major {
t.Skipf("host kernel (%d.%d) does not meet test's minimum required version: (%d.%d)",
k, m, kernel, major)
}
}

// setupInterface create a interface for testing
func setupInterface(conn *rtnetlink.Conn, name string, index, master uint32, driver rtnetlink.LinkDriver) error {
attrs := &rtnetlink.LinkAttributes{
Name: name,
Info: &rtnetlink.LinkInfo{Kind: driver.Kind(), Data: driver},
}
flag := uint32(unix.IFF_UP)
if master > 0 {
attrs.Master = &master
flag = 0
}
// construct an interface to test drivers
err := conn.Link.New(&rtnetlink.LinkMessage{
Family: unix.AF_UNSPEC,
Index: index,
Flags: flag,
Change: flag,
Attributes: attrs,
})
if err != nil {
conn.Link.Delete(index)
}
return err
}

func getInterface(conn *rtnetlink.Conn, index uint32) (*rtnetlink.LinkMessage, error) {
interf, err := conn.Link.Get(index)
if err != nil {
conn.Link.Delete(interf.Index)
return nil, err
}
return &interf, err
}

// creates a network namespace by utilizing ip commandline tool
// returns NetNS and clean function
func createNS(name string) (*rtnetlink.NetNS, func(), error) {
cmdPath, err := exec.LookPath("ip")
if err != nil {
return nil, nil, fmt.Errorf("getting ip command path failed, %w", err)
}
_, err = exec.Command(cmdPath, "netns", "add", name).Output()
if err != nil {
return nil, nil, fmt.Errorf("ip netns add %s, failed: %w", name, err)
}

ns, err := rtnetlink.NewNetNS(name)
if err != nil {
return nil, nil, fmt.Errorf("reading ns %s, failed: %w", name, err)
}
return ns, func() {
if err := ns.Close(); err != nil {
fmt.Printf("closing ns file failed: %v", err)
}
_, err := exec.Command(cmdPath, "netns", "del", name).Output()
if err != nil {
fmt.Printf("removing netns %s failed, %v", name, err)
}
}, nil
}
Loading

0 comments on commit bd79d59

Please sign in to comment.