mirror of
https://github.com/qdm12/gluetun.git
synced 2026-07-31 17:33:22 +02:00
chore(all): replace netlink library for more flexibility (#3107)
This commit is contained in:
+59
-15
@@ -1,31 +1,75 @@
|
||||
package netlink
|
||||
|
||||
import (
|
||||
"github.com/vishvananda/netlink"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"github.com/jsimonetti/rtnetlink/rtnl"
|
||||
)
|
||||
|
||||
func (n *NetLink) AddrList(link Link, family int) (
|
||||
addresses []Addr, err error,
|
||||
func (n *NetLink) AddrList(linkIndex uint32, family uint8) (
|
||||
ipPrefixes []netip.Prefix, err error,
|
||||
) {
|
||||
netlinkLink := linkToNetlinkLink(&link)
|
||||
netlinkAddresses, err := netlink.AddrList(netlinkLink, family)
|
||||
conn, err := rtnl.Dial(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
ifc := &net.Interface{
|
||||
Index: int(linkIndex),
|
||||
}
|
||||
ipNets, err := conn.Addrs(ifc, int(family))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list addresses: %w", err)
|
||||
}
|
||||
|
||||
addresses = make([]Addr, len(netlinkAddresses))
|
||||
for i := range netlinkAddresses {
|
||||
addresses[i].Network = netIPNetToNetipPrefix(netlinkAddresses[i].IPNet)
|
||||
ipPrefixes = make([]netip.Prefix, len(ipNets))
|
||||
for i := range ipNets {
|
||||
ipPrefixes[i] = netIPNetToNetipPrefix(ipNets[i])
|
||||
}
|
||||
|
||||
return addresses, nil
|
||||
return ipPrefixes, nil
|
||||
}
|
||||
|
||||
func (n *NetLink) AddrReplace(link Link, addr Addr) error {
|
||||
netlinkLink := linkToNetlinkLink(&link)
|
||||
netlinkAddress := netlink.Addr{
|
||||
IPNet: netipPrefixToIPNet(addr.Network),
|
||||
func (n *NetLink) AddrReplace(linkIndex uint32, prefix netip.Prefix) error {
|
||||
conn, err := rtnl.Dial(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
ipNet := netipPrefixToIPNet(prefix)
|
||||
|
||||
// Remove any address identical to the one we want to add
|
||||
family := FamilyV4
|
||||
if prefix.Addr().Is6() {
|
||||
family = FamilyV6
|
||||
}
|
||||
ifc := &net.Interface{
|
||||
Index: int(linkIndex),
|
||||
}
|
||||
addresses, err := conn.Addrs(ifc, int(family))
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing addresses: %w", err)
|
||||
}
|
||||
for _, address := range addresses {
|
||||
if address.IP.Equal(ipNet.IP) &&
|
||||
net.IP(address.Mask).String() == net.IP(ipNet.Mask).String() {
|
||||
err = conn.AddrDel(ifc, address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting address from interface: %w", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return netlink.AddrReplace(netlinkLink, &netlinkAddress)
|
||||
// Add the new address to the interface
|
||||
err = conn.AddrAdd(ifc, ipNet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("adding address to interface: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -36,6 +36,30 @@ func netIPNetToNetipPrefix(ipNet *net.IPNet) (prefix netip.Prefix) {
|
||||
return netip.PrefixFrom(ip, bits)
|
||||
}
|
||||
|
||||
func ipAndLengthToPrefix(ip *net.IP, length uint8) netip.Prefix {
|
||||
if ip == nil || len(*ip) == 0 {
|
||||
return netip.Prefix{}
|
||||
}
|
||||
var dstIP netip.Addr
|
||||
if ipv4 := ip.To4(); ipv4 != nil { // IPv6
|
||||
dstIP = netip.AddrFrom4([4]byte(*ip))
|
||||
} else {
|
||||
dstIP = netip.AddrFrom16([16]byte(*ip))
|
||||
}
|
||||
return netip.PrefixFrom(dstIP, int(length))
|
||||
}
|
||||
|
||||
func prefixToIPAndLength(prefix netip.Prefix) (ip *net.IP, length uint8) {
|
||||
if !prefix.IsValid() {
|
||||
return nil, 0
|
||||
}
|
||||
prefixIP := prefix.Addr().Unmap()
|
||||
ip = new(net.IP)
|
||||
*ip = netipAddrToNetIP(prefixIP)
|
||||
length = uint8(prefix.Bits()) //nolint:gosec
|
||||
return ip, length
|
||||
}
|
||||
|
||||
func netipAddrToNetIP(address netip.Addr) (ip net.IP) {
|
||||
switch {
|
||||
case !address.IsValid():
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func FamilyToString(family int) string {
|
||||
func FamilyToString(family uint8) string {
|
||||
switch family {
|
||||
case FamilyAll:
|
||||
return "all"
|
||||
|
||||
@@ -3,7 +3,7 @@ package netlink
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
const (
|
||||
FamilyAll = unix.AF_UNSPEC
|
||||
FamilyV4 = unix.AF_INET
|
||||
FamilyV6 = unix.AF_INET6
|
||||
FamilyAll uint8 = unix.AF_UNSPEC
|
||||
FamilyV4 uint8 = unix.AF_INET
|
||||
FamilyV6 uint8 = unix.AF_INET6
|
||||
)
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
package netlink
|
||||
|
||||
import (
|
||||
"math/rand/v2"
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/log"
|
||||
)
|
||||
|
||||
func ptrTo[T any](v T) *T { return &v }
|
||||
|
||||
func makeNetipPrefix(n byte) netip.Prefix {
|
||||
const bits = 24
|
||||
return netip.PrefixFrom(netip.AddrFrom4([4]byte{n, n, n, 0}), bits)
|
||||
}
|
||||
|
||||
var rng = rand.New(rand.NewChaCha8([32]byte{})) //nolint:gosec,gochecknoglobals
|
||||
|
||||
func makeLinkName() string {
|
||||
const alphabet = "abcdefghijklmnopqrstuvwxyz"
|
||||
name := make([]byte, 8)
|
||||
for i := range name {
|
||||
name[i] = alphabet[rng.IntN(len(alphabet))]
|
||||
}
|
||||
return "test" + string(name)
|
||||
}
|
||||
|
||||
type noopLogger struct{}
|
||||
|
||||
func (l *noopLogger) Debug(_ string) {}
|
||||
|
||||
@@ -19,7 +19,7 @@ func (n *NetLink) IsIPv6Supported() (supported bool, err error) {
|
||||
return false, fmt.Errorf("finding link corresponding to route: %w", err)
|
||||
}
|
||||
|
||||
sourceIsIPv6 := route.Src.IsValid() && route.Src.Is6()
|
||||
sourceIsIPv6 := route.Src.Addr().IsValid() && route.Src.Addr().Is6()
|
||||
destinationIsIPv6 := route.Dst.IsValid() && route.Dst.Addr().Is6()
|
||||
switch {
|
||||
case !sourceIsIPv6 && !destinationIsIPv6,
|
||||
|
||||
+162
-78
@@ -1,107 +1,191 @@
|
||||
package netlink
|
||||
|
||||
import "github.com/vishvananda/netlink"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jsimonetti/rtnetlink"
|
||||
)
|
||||
|
||||
type DeviceType uint16
|
||||
|
||||
type Link struct {
|
||||
Index uint32
|
||||
Name string
|
||||
DeviceType DeviceType
|
||||
VirtualType string
|
||||
MTU uint32
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkList() (links []Link, err error) {
|
||||
netlinkLinks, err := netlink.LinkList()
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
linkMessages, err := conn.Link.List()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing interfaces: %w", err)
|
||||
}
|
||||
|
||||
links = make([]Link, len(netlinkLinks))
|
||||
for i := range netlinkLinks {
|
||||
links[i] = netlinkLinkToLink(netlinkLinks[i])
|
||||
links = make([]Link, len(linkMessages))
|
||||
for i, message := range linkMessages {
|
||||
virtualType := ""
|
||||
if message.Attributes.Info != nil {
|
||||
virtualType = message.Attributes.Info.Kind
|
||||
}
|
||||
links[i] = Link{
|
||||
Index: message.Index,
|
||||
Name: message.Attributes.Name,
|
||||
DeviceType: DeviceType(message.Type),
|
||||
VirtualType: virtualType,
|
||||
MTU: message.Attributes.MTU,
|
||||
}
|
||||
}
|
||||
|
||||
return links, nil
|
||||
}
|
||||
|
||||
var ErrLinkNotFound = errors.New("link not found")
|
||||
|
||||
func (n *NetLink) LinkByName(name string) (link Link, err error) {
|
||||
netlinkLink, err := netlink.LinkByName(name)
|
||||
links, err := n.LinkList()
|
||||
if err != nil {
|
||||
return Link{}, err
|
||||
return Link{}, fmt.Errorf("listing links: %w", err)
|
||||
}
|
||||
|
||||
return netlinkLinkToLink(netlinkLink), nil
|
||||
for _, link := range links {
|
||||
if link.Name == name {
|
||||
return link, nil
|
||||
}
|
||||
}
|
||||
|
||||
return Link{}, fmt.Errorf("%w: for name %s", ErrLinkNotFound, name)
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkByIndex(index int) (link Link, err error) {
|
||||
netlinkLink, err := netlink.LinkByIndex(index)
|
||||
func (n *NetLink) LinkByIndex(index uint32) (link Link, err error) {
|
||||
links, err := n.LinkList()
|
||||
if err != nil {
|
||||
return Link{}, err
|
||||
return Link{}, fmt.Errorf("listing links: %w", err)
|
||||
}
|
||||
|
||||
return netlinkLinkToLink(netlinkLink), nil
|
||||
for _, link = range links {
|
||||
if link.Index == index {
|
||||
return link, nil
|
||||
}
|
||||
}
|
||||
|
||||
return Link{}, fmt.Errorf("%w: for index %d", ErrLinkNotFound, index)
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkAdd(link Link) (linkIndex int, err error) {
|
||||
netlinkLink := linkToNetlinkLink(&link)
|
||||
err = netlink.LinkAdd(netlinkLink)
|
||||
func (n *NetLink) LinkAdd(link Link) (linkIndex uint32, err error) {
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return 0, fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
return netlinkLink.Attrs().Index, nil
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
func (n *NetLink) LinkDel(link Link) (err error) {
|
||||
return netlink.LinkDel(linkToNetlinkLink(&link))
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkSetUp(link Link) (linkIndex int, err error) {
|
||||
netlinkLink := linkToNetlinkLink(&link)
|
||||
err = netlink.LinkSetUp(netlinkLink)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return netlinkLink.Attrs().Index, nil
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkSetDown(link Link) (err error) {
|
||||
return netlink.LinkSetDown(linkToNetlinkLink(&link))
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkSetMTU(link Link, mtu uint32) error {
|
||||
return netlink.LinkSetMTU(linkToNetlinkLink(&link), int(mtu))
|
||||
}
|
||||
|
||||
type netlinkLinkImpl struct {
|
||||
attrs *netlink.LinkAttrs
|
||||
linkType string
|
||||
}
|
||||
|
||||
func (n *netlinkLinkImpl) Attrs() *netlink.LinkAttrs {
|
||||
return n.attrs
|
||||
}
|
||||
|
||||
func (n *netlinkLinkImpl) Type() string {
|
||||
return n.linkType
|
||||
}
|
||||
|
||||
func netlinkLinkToLink(netlinkLink netlink.Link) Link {
|
||||
attributes := netlinkLink.Attrs()
|
||||
return Link{
|
||||
Type: netlinkLink.Type(),
|
||||
Name: attributes.Name,
|
||||
Index: attributes.Index,
|
||||
EncapType: attributes.EncapType,
|
||||
MTU: uint16(attributes.MTU), //nolint:gosec
|
||||
}
|
||||
}
|
||||
|
||||
// Warning: we must return `netlink.Link` and not `netlinkLinkImpl`
|
||||
// so that the vishvananda/netlink package can compare the returned
|
||||
// value against an untyped nil.
|
||||
func linkToNetlinkLink(link *Link) netlink.Link {
|
||||
if link == nil {
|
||||
return nil
|
||||
}
|
||||
return &netlinkLinkImpl{
|
||||
linkType: link.Type,
|
||||
attrs: &netlink.LinkAttrs{
|
||||
Name: link.Name,
|
||||
Index: link.Index,
|
||||
EncapType: link.EncapType,
|
||||
MTU: int(link.MTU),
|
||||
tx := &rtnetlink.LinkMessage{
|
||||
Type: uint16(link.DeviceType),
|
||||
Attributes: &rtnetlink.LinkAttributes{
|
||||
MTU: link.MTU,
|
||||
Name: link.Name,
|
||||
},
|
||||
}
|
||||
if link.VirtualType != "" {
|
||||
tx.Attributes.Info = &rtnetlink.LinkInfo{
|
||||
Kind: link.VirtualType,
|
||||
}
|
||||
}
|
||||
|
||||
err = conn.Link.New(tx)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("creating new link: %w", err)
|
||||
}
|
||||
|
||||
linkMessages, err := conn.Link.List()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("listing links: %w", err)
|
||||
}
|
||||
for _, linkMessage := range linkMessages {
|
||||
if linkMessage.Attributes.Name == link.Name {
|
||||
return linkMessage.Index, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("%w: matching name %s", ErrLinkNotFound, link.Name)
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkDel(linkIndex uint32) (err error) {
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
return conn.Link.Delete(linkIndex)
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkSetUp(linkIndex uint32) (err error) {
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
rx, err := conn.Link.Get(linkIndex)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting link: %w", err)
|
||||
}
|
||||
tx := &rtnetlink.LinkMessage{
|
||||
Type: rx.Type,
|
||||
Index: linkIndex,
|
||||
Flags: iffUp,
|
||||
Change: iffUp,
|
||||
}
|
||||
return conn.Link.Set(tx)
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkSetDown(linkIndex uint32) (err error) {
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
linkInfo, err := conn.Link.Get(linkIndex)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting link: %w", err)
|
||||
}
|
||||
message := &rtnetlink.LinkMessage{
|
||||
Type: linkInfo.Type,
|
||||
Index: linkIndex,
|
||||
Flags: 0,
|
||||
Change: iffUp,
|
||||
}
|
||||
return conn.Link.Set(message)
|
||||
}
|
||||
|
||||
func (n *NetLink) LinkSetMTU(linkIndex, mtu uint32) error {
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
message := &rtnetlink.LinkMessage{
|
||||
Index: linkIndex,
|
||||
Attributes: &rtnetlink.LinkAttributes{
|
||||
MTU: mtu,
|
||||
},
|
||||
}
|
||||
|
||||
err = conn.Link.Set(message)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting MTU to %d for link at index %d: %w",
|
||||
mtu, linkIndex, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package netlink
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
const (
|
||||
DeviceTypeEthernet DeviceType = unix.ARPHRD_ETHER
|
||||
DeviceTypeLoopback DeviceType = unix.ARPHRD_LOOPBACK
|
||||
DeviceTypeNone DeviceType = unix.ARPHRD_NONE
|
||||
|
||||
iffUp = unix.IFF_UP
|
||||
)
|
||||
@@ -0,0 +1,85 @@
|
||||
//go:build linux
|
||||
|
||||
package netlink
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_NetLink_LinkList(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
netlink := &NetLink{}
|
||||
|
||||
initialLinks, err := netlink.LinkList()
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, initialLinks)
|
||||
|
||||
loopbackFound := false
|
||||
for _, link := range initialLinks {
|
||||
if link.Name != "lo" {
|
||||
continue
|
||||
}
|
||||
loopbackFound = true
|
||||
assert.Equal(t, DeviceTypeLoopback, link.DeviceType)
|
||||
break
|
||||
}
|
||||
assert.True(t, loopbackFound, "loopback interface not found")
|
||||
|
||||
testLink := Link{
|
||||
Name: makeLinkName(),
|
||||
// note if [Link.VirtualType] is set, [Link.DeviceType]
|
||||
// is ignored and gets set to [DeviceTypeNone] in LinkAdd.
|
||||
DeviceType: DeviceTypeNone,
|
||||
VirtualType: "wireguard",
|
||||
MTU: 1420,
|
||||
}
|
||||
index, err := netlink.LinkAdd(testLink)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
_ = netlink.LinkDel(index)
|
||||
})
|
||||
|
||||
links, err := netlink.LinkList()
|
||||
require.NoError(t, err)
|
||||
|
||||
testLink.Index = index
|
||||
for _, link := range links {
|
||||
if link.Name != testLink.Name {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, testLink, link)
|
||||
return
|
||||
}
|
||||
t.Errorf("created link %q not found", testLink.Name)
|
||||
}
|
||||
|
||||
func Test_NetLink_LinkSetMTU(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
netlink := &NetLink{}
|
||||
|
||||
testLink := Link{
|
||||
Name: makeLinkName(),
|
||||
DeviceType: DeviceTypeNone,
|
||||
VirtualType: "wireguard",
|
||||
MTU: 1420,
|
||||
}
|
||||
index, err := netlink.LinkAdd(testLink)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
_ = netlink.LinkDel(index)
|
||||
})
|
||||
testLink.Index = index
|
||||
|
||||
err = netlink.LinkSetMTU(index, 1500)
|
||||
require.NoError(t, err)
|
||||
|
||||
link, err := netlink.LinkByIndex(index)
|
||||
require.NoError(t, err)
|
||||
testLink.MTU = 1500
|
||||
assert.Equal(t, testLink, link)
|
||||
}
|
||||
@@ -5,16 +5,41 @@ package netlink
|
||||
const (
|
||||
// FamilyAll is a placeholder only and should not
|
||||
// be used.
|
||||
FamilyAll = iota
|
||||
FamilyAll uint8 = iota
|
||||
// FamilyV4 is a placeholder only and should not
|
||||
// be used.
|
||||
FamilyV4
|
||||
// FamilyV6 is a placeholder only and should not
|
||||
// be used.
|
||||
FamilyV6
|
||||
|
||||
// DeviceTypeEthernet is a placeholder only and should not be used.
|
||||
DeviceTypeEthernet DeviceType = 0
|
||||
// DeviceTypeLoopback is a placeholder only and should not be used.
|
||||
DeviceTypeLoopback DeviceType = 0
|
||||
// DeviceTypeNone is a placeholder only and should not be used.
|
||||
DeviceTypeNone DeviceType = 0
|
||||
|
||||
// iffUp is a placeholder only and should not be used.
|
||||
iffUp = 0
|
||||
|
||||
// RouteTypeUnicast is a placeholder only and should not be used.
|
||||
RouteTypeUnicast = 0
|
||||
// ScopeUniverse is a placeholder only and should not be used.
|
||||
ScopeUniverse = 0
|
||||
// ProtoStatic is a placeholder only and should not be used.
|
||||
ProtoStatic = 0
|
||||
|
||||
// FlagInvert is a placeholder only and should not be used.
|
||||
FlagInvert = 0
|
||||
// ActionToTable is a placeholder only and should not be used.
|
||||
ActionToTable = 0
|
||||
|
||||
// rtTableCompat is a placeholder only and should not be used.
|
||||
rtTableCompat = 0
|
||||
)
|
||||
|
||||
func (n *NetLink) RuleList(family int) (rules []Rule, err error) {
|
||||
func (n *NetLink) RuleList(family uint8) (rules []Rule, err error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
@@ -26,6 +51,6 @@ func (n *NetLink) RuleDel(rule Rule) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (n *NetLink) IsWireguardSupported() bool {
|
||||
func (n *NetLink) IsWireguardSupported() (bool, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
+102
-44
@@ -1,67 +1,125 @@
|
||||
package netlink
|
||||
|
||||
import (
|
||||
"github.com/vishvananda/netlink"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"github.com/jsimonetti/rtnetlink"
|
||||
)
|
||||
|
||||
func (n *NetLink) RouteList(family int) (routes []Route, err error) {
|
||||
// We set the filter to netlink.RT_FILTER_TABLE so that
|
||||
// routes from all tables are listed, as long as the filter
|
||||
// table is set to 0.
|
||||
const filterMask = netlink.RT_FILTER_TABLE
|
||||
// The filter is not left to `nil` otherwise non-main tables
|
||||
// are ignored.
|
||||
filter := &netlink.Route{}
|
||||
type Route struct {
|
||||
LinkIndex uint32
|
||||
Dst netip.Prefix
|
||||
Src netip.Prefix
|
||||
Gw netip.Addr
|
||||
Priority uint32
|
||||
Family uint8
|
||||
Table uint32
|
||||
Type uint8
|
||||
Scope uint8
|
||||
Proto uint8
|
||||
}
|
||||
|
||||
netlinkRoutes, err := netlink.RouteListFiltered(family, filter, filterMask)
|
||||
func (r *Route) fromMessage(message rtnetlink.RouteMessage) {
|
||||
table := uint32(message.Table)
|
||||
if table == 0 || table == rtTableCompat {
|
||||
table = message.Attributes.Table
|
||||
}
|
||||
r.LinkIndex = message.Attributes.OutIface
|
||||
r.Dst = ipAndLengthToPrefix(&message.Attributes.Dst, message.DstLength)
|
||||
r.Src = ipAndLengthToPrefix(&message.Attributes.Src, message.SrcLength)
|
||||
r.Gw = netIPToNetipAddress(message.Attributes.Gateway)
|
||||
r.Priority = message.Attributes.Priority
|
||||
r.Family = message.Family
|
||||
r.Table = table
|
||||
r.Type = message.Type
|
||||
r.Scope = message.Scope
|
||||
r.Proto = message.Protocol
|
||||
}
|
||||
|
||||
func (r Route) message() *rtnetlink.RouteMessage {
|
||||
dst, dstLength := prefixToIPAndLength(r.Dst)
|
||||
src, srcLength := prefixToIPAndLength(r.Src)
|
||||
var table uint8
|
||||
var extendedTable uint32
|
||||
if r.Table <= uint32(^uint8(0)) {
|
||||
table = uint8(r.Table)
|
||||
} else {
|
||||
table = rtTableCompat
|
||||
extendedTable = r.Table
|
||||
}
|
||||
message := &rtnetlink.RouteMessage{
|
||||
Family: r.Family,
|
||||
DstLength: dstLength,
|
||||
SrcLength: srcLength,
|
||||
Table: table,
|
||||
Type: r.Type,
|
||||
Scope: r.Scope,
|
||||
Protocol: r.Proto,
|
||||
Attributes: rtnetlink.RouteAttributes{
|
||||
OutIface: r.LinkIndex,
|
||||
Dst: *dst, // there should always be a dst for routes
|
||||
Gateway: netipAddrToNetIP(r.Gw),
|
||||
Priority: r.Priority,
|
||||
Table: extendedTable,
|
||||
},
|
||||
}
|
||||
if src != nil { // src is optional
|
||||
message.Attributes.Src = *src
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
func (n *NetLink) RouteList(family uint8) (routes []Route, err error) {
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
routeMessages, err := conn.Route.List()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing interfaces: %w", err)
|
||||
}
|
||||
|
||||
routes = make([]Route, len(netlinkRoutes))
|
||||
for i := range netlinkRoutes {
|
||||
routes[i] = netlinkRouteToRoute(netlinkRoutes[i])
|
||||
routes = make([]Route, 0, len(routeMessages))
|
||||
for _, routeMessage := range routeMessages {
|
||||
if family != FamilyAll && routeMessage.Family != family {
|
||||
continue
|
||||
}
|
||||
var route Route
|
||||
route.fromMessage(routeMessage)
|
||||
routes = append(routes, route)
|
||||
}
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
func (n *NetLink) RouteAdd(route Route) error {
|
||||
netlinkRoute := routeToNetlinkRoute(route)
|
||||
return netlink.RouteAdd(&netlinkRoute)
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
return conn.Route.Add(route.message())
|
||||
}
|
||||
|
||||
func (n *NetLink) RouteDel(route Route) error {
|
||||
netlinkRoute := routeToNetlinkRoute(route)
|
||||
return netlink.RouteDel(&netlinkRoute)
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
return conn.Route.Delete(route.message())
|
||||
}
|
||||
|
||||
func (n *NetLink) RouteReplace(route Route) error {
|
||||
netlinkRoute := routeToNetlinkRoute(route)
|
||||
return netlink.RouteReplace(&netlinkRoute)
|
||||
}
|
||||
|
||||
func netlinkRouteToRoute(netlinkRoute netlink.Route) (route Route) {
|
||||
return Route{
|
||||
LinkIndex: netlinkRoute.LinkIndex,
|
||||
Dst: netIPNetToNetipPrefix(netlinkRoute.Dst),
|
||||
Src: netIPToNetipAddress(netlinkRoute.Src),
|
||||
Gw: netIPToNetipAddress(netlinkRoute.Gw),
|
||||
Priority: netlinkRoute.Priority,
|
||||
Family: netlinkRoute.Family,
|
||||
Table: netlinkRoute.Table,
|
||||
Type: netlinkRoute.Type,
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
func routeToNetlinkRoute(route Route) (netlinkRoute netlink.Route) {
|
||||
return netlink.Route{
|
||||
LinkIndex: route.LinkIndex,
|
||||
Dst: netipPrefixToIPNet(route.Dst),
|
||||
Src: netipAddrToNetIP(route.Src),
|
||||
Gw: netipAddrToNetIP(route.Gw),
|
||||
Priority: route.Priority,
|
||||
Family: route.Family,
|
||||
Table: route.Table,
|
||||
Type: route.Type,
|
||||
}
|
||||
return conn.Route.Replace(route.message())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package netlink
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
const (
|
||||
RouteTypeUnicast = unix.RTN_UNICAST
|
||||
ScopeUniverse = unix.RT_SCOPE_UNIVERSE
|
||||
ProtoStatic = unix.RTPROT_STATIC
|
||||
|
||||
rtTableCompat = unix.RT_TABLE_COMPAT
|
||||
)
|
||||
+80
-39
@@ -2,54 +2,95 @@ package netlink
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
"github.com/jsimonetti/rtnetlink"
|
||||
)
|
||||
|
||||
func NewRule() Rule {
|
||||
// defaults found from netlink.NewRule() for fields we use,
|
||||
// the rest of the defaults is set when converting from a `Rule`
|
||||
// to a `netlink.Rule`
|
||||
return Rule{
|
||||
Priority: -1,
|
||||
Mark: 0,
|
||||
type Rule struct {
|
||||
Priority *uint32
|
||||
Family uint8
|
||||
Table uint32
|
||||
Mark *uint32
|
||||
Src netip.Prefix
|
||||
Dst netip.Prefix
|
||||
Flags uint32
|
||||
Action uint8
|
||||
}
|
||||
|
||||
func (r *Rule) fromMessage(message rtnetlink.RuleMessage) {
|
||||
table := uint32(message.Table)
|
||||
if table == 0 || table == rtTableCompat {
|
||||
table = *message.Attributes.Table
|
||||
}
|
||||
r.Priority = message.Attributes.Priority
|
||||
r.Family = message.Family
|
||||
r.Table = table
|
||||
r.Mark = message.Attributes.FwMark
|
||||
r.Src = ipAndLengthToPrefix(message.Attributes.Src, message.SrcLength)
|
||||
r.Dst = ipAndLengthToPrefix(message.Attributes.Dst, message.DstLength)
|
||||
r.Flags = message.Flags
|
||||
r.Action = message.Action
|
||||
}
|
||||
|
||||
func ruleToNetlinkRule(rule Rule) (netlinkRule netlink.Rule) {
|
||||
netlinkRule = *netlink.NewRule()
|
||||
netlinkRule.Priority = rule.Priority
|
||||
netlinkRule.Family = rule.Family
|
||||
netlinkRule.Table = rule.Table
|
||||
netlinkRule.Mark = rule.Mark
|
||||
netlinkRule.Src = netipPrefixToIPNet(rule.Src)
|
||||
netlinkRule.Dst = netipPrefixToIPNet(rule.Dst)
|
||||
netlinkRule.Invert = rule.Invert
|
||||
return netlinkRule
|
||||
}
|
||||
func (r Rule) message() *rtnetlink.RuleMessage {
|
||||
src, srcLength := prefixToIPAndLength(r.Src)
|
||||
dst, dstLength := prefixToIPAndLength(r.Dst)
|
||||
|
||||
func netlinkRuleToRule(netlinkRule netlink.Rule) (rule Rule) {
|
||||
return Rule{
|
||||
Priority: netlinkRule.Priority,
|
||||
Family: netlinkRule.Family,
|
||||
Table: netlinkRule.Table,
|
||||
Mark: netlinkRule.Mark,
|
||||
Src: netIPNetToNetipPrefix(netlinkRule.Src),
|
||||
Dst: netIPNetToNetipPrefix(netlinkRule.Dst),
|
||||
Invert: netlinkRule.Invert,
|
||||
message := &rtnetlink.RuleMessage{
|
||||
Family: r.Family,
|
||||
SrcLength: srcLength,
|
||||
DstLength: dstLength,
|
||||
Flags: r.Flags,
|
||||
Action: r.Action,
|
||||
Attributes: &rtnetlink.RuleAttributes{
|
||||
Priority: r.Priority,
|
||||
FwMark: r.Mark,
|
||||
Src: src,
|
||||
Dst: dst,
|
||||
},
|
||||
}
|
||||
|
||||
if r.Table <= uint32(^uint8(0)) {
|
||||
message.Table = uint8(r.Table)
|
||||
} else {
|
||||
message.Table = rtTableCompat
|
||||
message.Attributes.Table = &r.Table
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
func ruleDbgMsg(add bool, rule Rule) (debugMessage string) {
|
||||
func (r Rule) String() string {
|
||||
from := "all"
|
||||
if r.Src.IsValid() && !r.Src.Addr().IsUnspecified() {
|
||||
from = r.Src.String()
|
||||
}
|
||||
|
||||
to := "all"
|
||||
if r.Dst.IsValid() && !r.Dst.Addr().IsUnspecified() {
|
||||
to = r.Dst.String()
|
||||
}
|
||||
|
||||
priority := ""
|
||||
if r.Priority != nil {
|
||||
priority = fmt.Sprintf(" %d", *r.Priority)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("ip rule%s: from %s to %s table %d",
|
||||
priority, from, to, r.Table)
|
||||
}
|
||||
|
||||
func (r Rule) debugMessage(add bool) (debugMessage string) {
|
||||
debugMessage = "ip"
|
||||
|
||||
switch rule.Family {
|
||||
switch r.Family {
|
||||
case FamilyV4:
|
||||
debugMessage += " -f inet"
|
||||
case FamilyV6:
|
||||
debugMessage += " -f inet6"
|
||||
default:
|
||||
debugMessage += " -f " + fmt.Sprint(rule.Family)
|
||||
debugMessage += " -f " + fmt.Sprint(r.Family)
|
||||
}
|
||||
|
||||
debugMessage += " rule"
|
||||
@@ -60,20 +101,20 @@ func ruleDbgMsg(add bool, rule Rule) (debugMessage string) {
|
||||
debugMessage += " del"
|
||||
}
|
||||
|
||||
if rule.Src.IsValid() {
|
||||
debugMessage += " from " + rule.Src.String()
|
||||
if r.Src.IsValid() {
|
||||
debugMessage += " from " + r.Src.String()
|
||||
}
|
||||
|
||||
if rule.Dst.IsValid() {
|
||||
debugMessage += " to " + rule.Dst.String()
|
||||
if r.Dst.IsValid() {
|
||||
debugMessage += " to " + r.Dst.String()
|
||||
}
|
||||
|
||||
if rule.Table != 0 {
|
||||
debugMessage += " lookup " + fmt.Sprint(rule.Table)
|
||||
if r.Table != 0 {
|
||||
debugMessage += " lookup " + fmt.Sprint(r.Table)
|
||||
}
|
||||
|
||||
if rule.Priority != -1 {
|
||||
debugMessage += " pref " + fmt.Sprint(rule.Priority)
|
||||
if r.Priority != nil {
|
||||
debugMessage += " pref " + fmt.Sprint(*r.Priority)
|
||||
}
|
||||
|
||||
return debugMessage
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
package netlink
|
||||
|
||||
import "github.com/vishvananda/netlink"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
func (n *NetLink) RuleList(family int) (rules []Rule, err error) {
|
||||
"github.com/jsimonetti/rtnetlink"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
FlagInvert = unix.FIB_RULE_INVERT
|
||||
ActionToTable = unix.FR_ACT_TO_TBL
|
||||
)
|
||||
|
||||
func (n *NetLink) RuleList(family uint8) (rules []Rule, err error) {
|
||||
switch family {
|
||||
case FamilyAll:
|
||||
n.debugLogger.Debug("ip -4 rule list")
|
||||
@@ -12,26 +22,48 @@ func (n *NetLink) RuleList(family int) (rules []Rule, err error) {
|
||||
case FamilyV6:
|
||||
n.debugLogger.Debug("ip -6 rule list")
|
||||
}
|
||||
netlinkRules, err := netlink.RuleList(family)
|
||||
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
ruleMessages, err := conn.Rule.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rules = make([]Rule, len(netlinkRules))
|
||||
for i := range netlinkRules {
|
||||
rules[i] = netlinkRuleToRule(netlinkRules[i])
|
||||
rules = make([]Rule, 0, len(ruleMessages))
|
||||
for _, message := range ruleMessages {
|
||||
if family != FamilyAll && family != message.Family {
|
||||
continue
|
||||
}
|
||||
var rule Rule
|
||||
rule.fromMessage(message)
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func (n *NetLink) RuleAdd(rule Rule) error {
|
||||
n.debugLogger.Debug(ruleDbgMsg(true, rule))
|
||||
netlinkRule := ruleToNetlinkRule(rule)
|
||||
return netlink.RuleAdd(&netlinkRule)
|
||||
n.debugLogger.Debug(rule.debugMessage(true))
|
||||
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
return conn.Rule.Add(rule.message())
|
||||
}
|
||||
|
||||
func (n *NetLink) RuleDel(rule Rule) error {
|
||||
n.debugLogger.Debug(ruleDbgMsg(false, rule))
|
||||
netlinkRule := ruleToNetlinkRule(rule)
|
||||
return netlink.RuleDel(&netlinkRule)
|
||||
n.debugLogger.Debug(rule.debugMessage(false))
|
||||
|
||||
conn, err := rtnetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
return conn.Rule.Delete(rule.message())
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_ruleDbgMsg(t *testing.T) {
|
||||
func Test_Rule_debugMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
@@ -15,7 +15,7 @@ func Test_ruleDbgMsg(t *testing.T) {
|
||||
dbgMsg string
|
||||
}{
|
||||
"default values": {
|
||||
dbgMsg: "ip -f 0 rule del pref 0",
|
||||
dbgMsg: "ip -f 0 rule del",
|
||||
},
|
||||
"add rule": {
|
||||
add: true,
|
||||
@@ -24,7 +24,7 @@ func Test_ruleDbgMsg(t *testing.T) {
|
||||
Src: makeNetipPrefix(1),
|
||||
Dst: makeNetipPrefix(2),
|
||||
Table: 100,
|
||||
Priority: 101,
|
||||
Priority: ptrTo(uint32(101)),
|
||||
},
|
||||
dbgMsg: "ip -f inet rule add from 1.1.1.0/24 to 2.2.2.0/24 lookup 100 pref 101",
|
||||
},
|
||||
@@ -34,7 +34,7 @@ func Test_ruleDbgMsg(t *testing.T) {
|
||||
Src: makeNetipPrefix(1),
|
||||
Dst: makeNetipPrefix(2),
|
||||
Table: 100,
|
||||
Priority: 101,
|
||||
Priority: ptrTo(uint32(101)),
|
||||
},
|
||||
dbgMsg: "ip -f inet rule del from 1.1.1.0/24 to 2.2.2.0/24 lookup 100 pref 101",
|
||||
},
|
||||
@@ -44,7 +44,7 @@ func Test_ruleDbgMsg(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbgMsg := ruleDbgMsg(testCase.add, testCase.rule)
|
||||
dbgMsg := testCase.rule.debugMessage(testCase.add)
|
||||
|
||||
assert.Equal(t, testCase.dbgMsg, dbgMsg)
|
||||
})
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
package netlink
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
type Addr struct {
|
||||
Network netip.Prefix
|
||||
}
|
||||
|
||||
func (a Addr) String() string {
|
||||
return a.Network.String()
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
Type string
|
||||
Name string
|
||||
Index int
|
||||
EncapType string
|
||||
MTU uint16
|
||||
}
|
||||
|
||||
type Route struct {
|
||||
LinkIndex int
|
||||
Dst netip.Prefix
|
||||
Src netip.Addr
|
||||
Gw netip.Addr
|
||||
Priority int
|
||||
Family int
|
||||
Table int
|
||||
Type int
|
||||
}
|
||||
|
||||
type Rule struct {
|
||||
Priority int
|
||||
Family int
|
||||
Table int
|
||||
Mark uint32
|
||||
Src netip.Prefix
|
||||
Dst netip.Prefix
|
||||
Invert bool
|
||||
}
|
||||
|
||||
func (r Rule) String() string {
|
||||
from := "all"
|
||||
if r.Src.IsValid() && !r.Src.Addr().IsUnspecified() {
|
||||
from = r.Src.String()
|
||||
}
|
||||
|
||||
to := "all"
|
||||
if r.Dst.IsValid() && !r.Dst.Addr().IsUnspecified() {
|
||||
to = r.Dst.String()
|
||||
}
|
||||
|
||||
return fmt.Sprintf("ip rule %d: from %s to %s table %d",
|
||||
r.Priority, from, to, r.Table)
|
||||
}
|
||||
@@ -1,35 +1,58 @@
|
||||
package netlink
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mdlayher/genetlink"
|
||||
"github.com/qdm12/gluetun/internal/mod"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
func (n *NetLink) IsWireguardSupported() bool {
|
||||
func (n *NetLink) IsWireguardSupported() (ok bool, err error) {
|
||||
// Check for Wireguard family without loading the wireguard module.
|
||||
// Some kernels have the wireguard module built-in, and don't have a
|
||||
// modules directory, such as WSL2 kernels.
|
||||
ok := hasWireguardFamily()
|
||||
if ok {
|
||||
return true
|
||||
ok, err = hasWireguardFamily()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("checking wireguard family: %w", err)
|
||||
} else if ok {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Try loading the wireguard module, since some systems do not load
|
||||
// it after a boot. If this fails, wireguard is assumed to not be supported.
|
||||
n.debugLogger.Debugf("wireguard family not found, trying to load wireguard kernel module")
|
||||
err := mod.Probe("wireguard")
|
||||
err = mod.Probe("wireguard")
|
||||
if err != nil {
|
||||
n.debugLogger.Debugf("failed loading wireguard kernel module: %s", err)
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
n.debugLogger.Debugf("wireguard kernel module loaded successfully")
|
||||
|
||||
// Re-check if the Wireguard family is now available, after loading
|
||||
// the wireguard kernel module.
|
||||
return hasWireguardFamily()
|
||||
ok, err = hasWireguardFamily()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("checking wireguard family: %w", err)
|
||||
}
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
func hasWireguardFamily() bool {
|
||||
_, err := netlink.GenlFamilyGet("wireguard")
|
||||
return err == nil
|
||||
func hasWireguardFamily() (ok bool, err error) {
|
||||
conn, err := genetlink.Dial(nil)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("dialing netlink: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = conn.GetFamily("wireguard")
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("getting wireguard family: %w", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ package netlink
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_NetLink_IsWireguardSupported(t *testing.T) {
|
||||
@@ -12,7 +14,8 @@ func Test_NetLink_IsWireguardSupported(t *testing.T) {
|
||||
netLink := &NetLink{
|
||||
debugLogger: &noopLogger{},
|
||||
}
|
||||
ok := netLink.IsWireguardSupported()
|
||||
ok, err := netLink.IsWireguardSupported()
|
||||
require.NoError(t, err)
|
||||
if ok { // cannot assert since this depends on kernel
|
||||
t.Log("wireguard is supported")
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user