Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
63 changes: 57 additions & 6 deletions chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,14 @@ type Chain struct {
Priority *ChainPriority
Type ChainType
Policy *ChainPolicy
Device string
// Device is the interface an ingress or egress chain is attached to.
// When a chain names several, this is the first of them and Devices
// holds all of them.
Device string
// Devices are all the interfaces an ingress or egress chain is
// attached to ("devices = { eth0, eth1 }"). Setting Device alone
// still works and is marshalled as before.
Devices []string
}

// AddChain adds the specified Chain. See also
Expand All @@ -121,7 +128,21 @@ func (cc *Conn) AddChain(c *Chain) *Chain {
{Type: unix.NFTA_HOOK_PRIORITY, Data: binaryutil.BigEndian.PutUint32(uint32(*c.Priority))},
}

if c.Device != "" {
switch {
case len(c.Devices) > 1:
var elems []netlink.Attribute
for _, d := range c.Devices {
elems = append(elems, netlink.Attribute{Type: nftaDeviceName, Data: []byte(d + "\x00")})
}
devs, err := netlink.MarshalAttributes(elems)
if err != nil {
cc.setErr(err)
return c
}
hookAttr = append(hookAttr, netlink.Attribute{Type: unix.NLA_F_NESTED | nftaHookDevs, Data: devs})
case len(c.Devices) == 1:
hookAttr = append(hookAttr, netlink.Attribute{Type: unix.NFTA_HOOK_DEV, Data: []byte(c.Devices[0] + "\x00")})
case c.Device != "":
hookAttr = append(hookAttr, netlink.Attribute{Type: unix.NFTA_HOOK_DEV, Data: []byte(c.Device + "\x00")})
}

Expand Down Expand Up @@ -311,7 +332,12 @@ func chainFromMsg(msg netlink.Message) (*Chain, error) {
c.Policy = &policy
case unix.NFTA_CHAIN_HOOK:
ad.Do(func(b []byte) error {
c.Hooknum, c.Priority, err = hookFromMsg(b)
var devices []string
c.Hooknum, c.Priority, devices, err = hookFromMsg(b)
if len(devices) > 0 {
c.Device = devices[0]
c.Devices = devices
}
return err
})
}
Expand All @@ -320,25 +346,50 @@ func chainFromMsg(msg netlink.Message) (*Chain, error) {
return &c, nil
}

func hookFromMsg(b []byte) (*ChainHook, *ChainPriority, error) {
// nftaHookDevs and nftaDeviceName are NFTA_HOOK_DEVS and NFTA_DEVICE_NAME
// from linux/netfilter/nf_tables.h, which golang.org/x/sys/unix does not
// export.
const (
nftaHookDevs = 4
nftaDeviceName = 1
)

func hookFromMsg(b []byte) (*ChainHook, *ChainPriority, []string, error) {
ad, err := netlink.NewAttributeDecoder(b)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}

ad.ByteOrder = binary.BigEndian

var hooknum ChainHook
var prio ChainPriority
var devices []string

for ad.Next() {
switch ad.Type() {
case unix.NFTA_HOOK_HOOKNUM:
hooknum = ChainHook(ad.Uint32())
case unix.NFTA_HOOK_PRIORITY:
prio = ChainPriority(ad.Uint32())
case unix.NFTA_HOOK_DEV:
devices = append(devices, ad.String())
case nftaHookDevs:
ad.Do(func(b []byte) error {
nested, err := netlink.NewAttributeDecoder(b)
if err != nil {
return err
}
nested.ByteOrder = binary.BigEndian
for nested.Next() {
if nested.Type() == nftaDeviceName {
devices = append(devices, nested.String())
}
}
return nested.Err()
})
}
}

return &hooknum, &prio, nil
return &hooknum, &prio, devices, nil
}
73 changes: 73 additions & 0 deletions chain_device_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package nftables

import (
"encoding/binary"
"testing"

"github.com/mdlayher/netlink"
"golang.org/x/sys/unix"
)

func hookAttrs(t *testing.T, extra ...netlink.Attribute) []byte {
t.Helper()
num := make([]byte, 4)
binary.BigEndian.PutUint32(num, uint32(*ChainHookIngress))
prio := make([]byte, 4)
binary.BigEndian.PutUint32(prio, uint32(*ChainPriorityFilter))
attrs := append([]netlink.Attribute{
{Type: unix.NFTA_HOOK_HOOKNUM, Data: num},
{Type: unix.NFTA_HOOK_PRIORITY, Data: prio},
}, extra...)
b, err := netlink.MarshalAttributes(attrs)
if err != nil {
t.Fatal(err)
}
return b
}

// A chain attached to one device: the kernel sends NFTA_HOOK_DEV, which
// was being read and discarded.
func TestHookFromMsgReadsASingleDevice(t *testing.T) {
_, _, devices, err := hookFromMsg(hookAttrs(t,
netlink.Attribute{Type: unix.NFTA_HOOK_DEV, Data: []byte("eth0\x00")}))
if err != nil {
t.Fatal(err)
}
if len(devices) != 1 || devices[0] != "eth0" {
t.Fatalf("devices = %v, want [eth0]", devices)
}
}

// "devices = { eth0, eth1 }" arrives as a nested list instead.
func TestHookFromMsgReadsADeviceList(t *testing.T) {
list, err := netlink.MarshalAttributes([]netlink.Attribute{
{Type: nftaDeviceName, Data: []byte("eth0\x00")},
{Type: nftaDeviceName, Data: []byte("eth1\x00")},
})
if err != nil {
t.Fatal(err)
}
_, _, devices, err := hookFromMsg(hookAttrs(t,
netlink.Attribute{Type: unix.NLA_F_NESTED | nftaHookDevs, Data: list}))
if err != nil {
t.Fatal(err)
}
if len(devices) != 2 || devices[0] != "eth0" || devices[1] != "eth1" {
t.Fatalf("devices = %v, want [eth0 eth1]", devices)
}
}

// A chain with no device, which is every base chain in the ip families,
// is unaffected.
func TestHookFromMsgWithoutADevice(t *testing.T) {
hooknum, prio, devices, err := hookFromMsg(hookAttrs(t))
if err != nil {
t.Fatal(err)
}
if len(devices) != 0 {
t.Errorf("devices = %v, want none", devices)
}
if hooknum == nil || prio == nil {
t.Error("the hook number and priority must still be read")
}
}