pkg/proxy/util/nfacct: utility to interact with nfacct subsystem

nfacct is netfilter's accounting subsystem. This utility allows
interactions with the subsystem using lower level netlink API.

Signed-off-by: Daman Arora <aroradaman@gmail.com>
This commit is contained in:
Daman Arora 2024-04-25 21:18:08 +05:30
parent 7b73ee018c
commit 6b5291654f
6 changed files with 1086 additions and 1 deletions

2
go.mod
View File

@ -60,6 +60,7 @@ require (
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.4
github.com/vishvananda/netlink v1.1.0
github.com/vishvananda/netns v0.0.4
go.etcd.io/etcd/api/v3 v3.5.13
go.etcd.io/etcd/client/pkg/v3 v3.5.13
go.etcd.io/etcd/client/v3 v3.5.13
@ -202,7 +203,6 @@ require (
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
github.com/xlab/treeprint v1.2.0 // indirect

View File

@ -0,0 +1,83 @@
//go:build linux
// +build linux
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package nfacct
import (
"github.com/vishvananda/netlink/nl"
"github.com/vishvananda/netns"
"golang.org/x/sys/unix"
)
// handler is an injectable interface for creating netlink request.
type handler interface {
newRequest(cmd int, flags uint16) request
}
// request is an injectable interface representing a netlink request.
type request interface {
Serialize() []byte
AddData(data nl.NetlinkRequestData)
AddRawData(data []byte)
Execute(sockType int, resType uint16) ([][]byte, error)
}
// netlinkHandler is an implementation of the handler interface. It maintains a netlink socket
// for communication with the NFAcct subsystem.
type netlinkHandler struct {
socket *nl.NetlinkSocket
}
// newNetlinkHandler initializes a netlink socket in the current network namespace and returns
// an instance of netlinkHandler with the initialized socket.
func newNetlinkHandler() (handler, error) {
socket, err := nl.GetNetlinkSocketAt(netns.None(), netns.None(), unix.NETLINK_NETFILTER)
if err != nil {
return nil, err
}
return &netlinkHandler{socket: socket}, nil
}
// newRequest creates a netlink request tailored for the NFAcct subsystem encapsulating the
// specified cmd and flags. It incorporates the netlink header and netfilter generic header
// into the resulting request.
func (n *netlinkHandler) newRequest(cmd int, flags uint16) request {
req := &nl.NetlinkRequest{
// netlink message header
// (definition: https://github.com/torvalds/linux/blob/v6.7/include/uapi/linux/netlink.h#L44-L58)
NlMsghdr: unix.NlMsghdr{
Len: uint32(unix.SizeofNlMsghdr),
Type: uint16(cmd | (unix.NFNL_SUBSYS_ACCT << 8)),
Flags: flags,
},
Sockets: map[int]*nl.SocketHandle{
unix.NETLINK_NETFILTER: {Socket: n.socket},
},
Data: []nl.NetlinkRequestData{
// netfilter generic message
// (definition: https://github.com/torvalds/linux/blob/v6.7/include/uapi/linux/netfilter/nfnetlink.h#L32-L38)
&nl.Nfgenmsg{
NfgenFamily: uint8(unix.AF_NETLINK),
Version: nl.NFNETLINK_V0,
ResId: 0,
},
},
}
return req
}

View File

@ -0,0 +1,36 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package nfacct
// Counter represents a nfacct accounting object.
type Counter struct {
Name string
Packets uint64
Bytes uint64
}
// Interface is an injectable interface for running nfacct commands.
type Interface interface {
// Ensure checks the existence of a nfacct counter with the provided name and creates it if absent.
Ensure(name string) error
// Add creates a nfacct counter with the given name, returning an error if it already exists.
Add(name string) error
// Get retrieves the nfacct counter with the specified name, returning an error if it doesn't exist.
Get(name string) (*Counter, error)
// List retrieves all nfacct counters.
List() ([]*Counter, error)
}

View File

@ -0,0 +1,307 @@
//go:build linux
// +build linux
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package nfacct
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"syscall"
"github.com/vishvananda/netlink/nl"
"golang.org/x/sys/unix"
)
// MaxLength represents the maximum length allowed for the name in a nfacct counter.
const MaxLength = 31
// nf netlink nfacct commands, these should strictly match with the ones defined in kernel headers.
// (definition: https://github.com/torvalds/linux/blob/v6.7/include/uapi/linux/netfilter/nfnetlink_acct.h#L9-L16)
const (
// NFNL_MSG_ACCT_NEW
cmdNew = 0
// NFNL_MSG_ACCT_GET
cmdGet = 1
)
// nf netlink nfacct attribute, these should strictly match with the ones defined in kernel headers.
// (definition: https://github.com/torvalds/linux/blob/v6.7/include/uapi/linux/netfilter/nfnetlink_acct.h#L24-L35)
const (
// NFACCT_NAME
attrName = 1
// NFACCT_PKTS
attrPackets = 2
// NFACCT_BYTES
attrBytes = 3
)
// runner implements the Interface and depends on the handler for execution.
type runner struct {
handler handler
}
// New returns a new Interface.
func New() (Interface, error) {
hndlr, err := newNetlinkHandler()
if err != nil {
return nil, err
}
return newInternal(hndlr)
}
// newInternal returns a new Interface with the given handler.
func newInternal(hndlr handler) (Interface, error) {
return &runner{handler: hndlr}, nil
}
// Ensure is part of the interface.
func (r *runner) Ensure(name string) error {
counter, err := r.Get(name)
if counter != nil {
return nil
}
if err != nil && errors.Is(err, ErrObjectNotFound) {
return handleError(r.Add(name))
} else if err != nil {
return handleError(err)
} else {
return ErrUnexpected
}
}
// Add is part of the interface.
func (r *runner) Add(name string) error {
if name == "" {
return ErrEmptyName
}
if len(name) > MaxLength {
return ErrNameExceedsMaxLength
}
req := r.handler.newRequest(cmdNew, unix.NLM_F_REQUEST|unix.NLM_F_CREATE|unix.NLM_F_ACK)
req.AddData(nl.NewRtAttr(attrName, nl.ZeroTerminated(name)))
_, err := req.Execute(unix.NETLINK_NETFILTER, 0)
if err != nil {
return handleError(err)
}
return nil
}
// Get is part of the interface.
func (r *runner) Get(name string) (*Counter, error) {
if len(name) > MaxLength {
return nil, ErrNameExceedsMaxLength
}
req := r.handler.newRequest(cmdGet, unix.NLM_F_REQUEST|unix.NLM_F_ACK)
req.AddData(nl.NewRtAttr(attrName, nl.ZeroTerminated(name)))
msgs, err := req.Execute(unix.NETLINK_NETFILTER, 0)
if err != nil {
return nil, handleError(err)
}
var counter *Counter
for _, msg := range msgs {
counter, err = decode(msg, true)
if err != nil {
return nil, handleError(err)
}
}
return counter, nil
}
// List is part of the interface.
func (r *runner) List() ([]*Counter, error) {
req := r.handler.newRequest(cmdGet, unix.NLM_F_REQUEST|unix.NLM_F_DUMP)
msgs, err := req.Execute(unix.NETLINK_NETFILTER, 0)
if err != nil {
return nil, handleError(err)
}
counters := make([]*Counter, 0)
for _, msg := range msgs {
counter, err := decode(msg, true)
if err != nil {
return nil, handleError(err)
}
counters = append(counters, counter)
}
return counters, nil
}
var ErrObjectNotFound = errors.New("object not found")
var ErrObjectAlreadyExists = errors.New("object already exists")
var ErrNameExceedsMaxLength = fmt.Errorf("object name exceeds the maximum allowed length of %d characters", MaxLength)
var ErrEmptyName = errors.New("object name cannot be empty")
var ErrUnexpected = errors.New("unexpected error")
func handleError(err error) error {
switch {
case err == nil:
return nil
case errors.Is(err, syscall.ENOENT):
return ErrObjectNotFound
case errors.Is(err, syscall.EBUSY):
return ErrObjectAlreadyExists
default:
return fmt.Errorf("%s: %s", ErrUnexpected.Error(), err.Error())
}
}
// decode function processes a byte stream, requiring the 'strict' parameter to be true in production and
// false only for testing purposes. If in strict mode and any of the relevant attributes (name, packets, or bytes)
// have not been processed, an error is returned indicating a failure to decode the byte stream.
//
// Parse the netlink message as per the documentation outlined in:
// https://docs.kernel.org/userspace-api/netlink/intro.html
//
// Message Components:
// - netfilter generic message [4 bytes]
// struct nfgenmsg (definition: https://github.com/torvalds/linux/blob/v6.7/include/uapi/linux/netfilter/nfnetlink.h#L32-L38)
// - attributes [variable-sized, must align to 4 bytes from the start of attribute]
// struct nlattr (definition: https://github.com/torvalds/linux/blob/v6.7/include/uapi/linux/netlink.h#L220-L232)
//
// Attribute Components:
// - length [2 bytes]
// length includes bytes for defining the length itself, bytes for defining the type,
// and the actual bytes of data without any padding.
// - type [2 bytes]
// - data [variable-sized]
// - padding [optional]
//
// Example. Counter{Name: "dummy-metric", Packets: 123, Bytes: 54321} in netlink message:
//
// struct nfgenmsg{
// __u8 nfgen_family: AF_NETLINK
// __u8 version: nl.NFNETLINK_V0
// __be16 res_id: nl.NFNETLINK_V0
// }
//
// struct nlattr{
// __u16 nla_len: 13
// __u16 nla_type: NFACCT_NAME
// char data: dummy-metric\0
// }
//
// (padding:)
// data: \0\0\0
//
// struct nlattr{
// __u16 nla_len: 12
// __u16 nla_type: NFACCT_PKTS
// __u64: data: 123
// }
//
// struct nlattr{
// __u16 nla_len: 12
// __u16 nla_type: NFACCT_BYTES
// __u64: data: 54321
// }
func decode(msg []byte, strict bool) (*Counter, error) {
counter := &Counter{}
reader := bytes.NewReader(msg)
// skip the first 4 bytes (netfilter generic message).
if _, err := reader.Seek(nl.SizeofNfgenmsg, io.SeekCurrent); err != nil {
return nil, err
}
// attrsProcessed tracks the number of processed attributes.
var attrsProcessed int
// length and type of netlink attribute.
var length, attrType uint16
// now we are just left with the attributes(struct nlattr) after skipping netlink generic
// message; we iterate over all the attributes one by one to construct our Counter object.
for reader.Len() > 0 {
// netlink attributes are in LTV(length, type and value) format.
// STEP 1. parse length [2 bytes]
if err := binary.Read(reader, binary.NativeEndian, &length); err != nil {
return nil, err
}
// STEP 2. parse type [2 bytes]
if err := binary.Read(reader, binary.NativeEndian, &attrType); err != nil {
return nil, err
}
// STEP 3. adjust the length
// adjust the length to consider the header bytes read in step(1) and step(2); the actual
// length of data will be 4 bytes less than the originally read value.
length -= 4
// STEP 4. parse value [variable sized]
// The value can assume any data-type. To read it into the appropriate data structure, we need
// to know the data type in advance. We achieve this by switching on the attribute-type, and we
// allocate the 'adjusted length' bytes (as done in step(3)) for the data-structure.
switch attrType {
case attrName:
// NFACCT_NAME has a variable size, so we allocate a slice of 'adjusted length' bytes
// and read the next 'adjusted length' bytes into this slice.
data := make([]byte, length)
if err := binary.Read(reader, binary.NativeEndian, data); err != nil {
return nil, err
}
counter.Name = string(data[:length-1])
attrsProcessed++
case attrPackets:
// NFACCT_PKTS holds 8 bytes of data, so we directly read the next 8 bytes into a 64-bit
// unsigned integer (counter.Packets).
if err := binary.Read(reader, binary.BigEndian, &counter.Packets); err != nil {
return nil, err
}
attrsProcessed++
case attrBytes:
// NFACCT_BYTES holds 8 bytes of data, so we directly read the next 8 bytes into a 64-bit
// unsigned integer (counter.Bytes).
if err := binary.Read(reader, binary.BigEndian, &counter.Bytes); err != nil {
return nil, err
}
attrsProcessed++
default:
// skip the data part for unknown attribute
if _, err := reader.Seek(int64(length), io.SeekCurrent); err != nil {
return nil, err
}
}
// Move past the padding to align with the fixed-size length, always a multiple of 4.
// If, for instance, the length is 9, skip 3 bytes of padding to reach the start of
// the next attribute.
// (ref: https://github.com/torvalds/linux/blob/v6.7/include/uapi/linux/netlink.h#L220-L227)
if length%4 != 0 {
padding := 4 - length%4
if _, err := reader.Seek(int64(padding), io.SeekCurrent); err != nil {
return nil, err
}
}
}
// return err if any of the required attribute is not processed.
if strict && attrsProcessed != 3 {
return nil, errors.New("failed to decode byte-stream")
}
return counter, nil
}

View File

@ -0,0 +1,628 @@
//go:build linux
// +build linux
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package nfacct
import (
"syscall"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vishvananda/netlink/nl"
"golang.org/x/sys/unix"
)
// fakeHandler is a mock implementation of the handler interface, designed for testing.
type fakeHandler struct {
// requests stores instances of fakeRequest, capturing new requests.
requests []*fakeRequest
// responses holds responses for the subsequent fakeRequest.Execute calls.
responses [][][]byte
// errs holds errors for the subsequent fakeRequest.Execute calls.
errs []error
}
// newRequest creates a request object with the given cmd, flags, predefined response and error.
// It additionally records the created request object.
func (fh *fakeHandler) newRequest(cmd int, flags uint16) request {
var response [][]byte
if fh.responses != nil && len(fh.responses) > 0 {
response = fh.responses[0]
// remove the response from the list of predefined responses and add it to request object for mocking.
fh.responses = fh.responses[1:]
}
var err error
if fh.errs != nil && len(fh.errs) > 0 {
err = fh.errs[0]
// remove the error from the list of predefined errors and add it to request object for mocking.
fh.errs = fh.errs[1:]
}
req := &fakeRequest{cmd: cmd, flags: flags, response: response, err: err}
fh.requests = append(fh.requests, req)
return req
}
// fakeRequest records information about the cmd and flags used when creating a new request,
// maintains a list for netlink attributes, and stores a predefined response and an optional
// error for subsequent execution.
type fakeRequest struct {
// cmd and flags which were used to create the request.
cmd int
flags uint16
// data holds netlink attributes.
data []nl.NetlinkRequestData
// response and err are the predefined output of execution.
response [][]byte
err error
}
// Serialize is part of request interface.
func (fr *fakeRequest) Serialize() []byte { return nil }
// AddData is part of request interface.
func (fr *fakeRequest) AddData(data nl.NetlinkRequestData) {
fr.data = append(fr.data, data)
}
// AddRawData is part of request interface.
func (fr *fakeRequest) AddRawData(_ []byte) {}
// Execute is part of request interface.
func (fr *fakeRequest) Execute(_ int, _ uint16) ([][]byte, error) {
return fr.response, fr.err
}
func TestRunner_Add(t *testing.T) {
testCases := []struct {
name string
counterName string
handler *fakeHandler
err error
netlinkCalls int
}{
{
name: "valid",
counterName: "metric-1",
handler: &fakeHandler{},
// expected calls: NFNL_MSG_ACCT_NEW
netlinkCalls: 1,
},
{
name: "add duplicate counter",
counterName: "metric-2",
handler: &fakeHandler{
errs: []error{syscall.EBUSY},
},
err: ErrObjectAlreadyExists,
// expected calls: NFNL_MSG_ACCT_NEW
netlinkCalls: 1,
},
{
name: "insufficient privilege",
counterName: "metric-2",
handler: &fakeHandler{
errs: []error{syscall.EPERM},
},
err: ErrUnexpected,
// expected calls: NFNL_MSG_ACCT_NEW
netlinkCalls: 1,
},
{
name: "exceeds max length",
counterName: "this-is-a-string-with-more-than-32-characters",
handler: &fakeHandler{},
err: ErrNameExceedsMaxLength,
// expected calls: zero (the error should be returned by this library)
netlinkCalls: 0,
},
{
name: "falls below min length",
counterName: "",
handler: &fakeHandler{},
err: ErrEmptyName,
// expected calls: zero (the error should be returned by this library)
netlinkCalls: 0,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
rnr, err := newInternal(tc.handler)
assert.NoError(t, err)
err = rnr.Add(tc.counterName)
if tc.err != nil {
assert.ErrorContains(t, err, tc.err.Error())
} else {
assert.NoError(t, err)
}
// validate number of requests
assert.Equal(t, tc.netlinkCalls, len(tc.handler.requests))
if tc.netlinkCalls > 0 {
// validate request
assert.Equal(t, cmdNew, tc.handler.requests[0].cmd)
assert.Equal(t, uint16(unix.NLM_F_REQUEST|unix.NLM_F_CREATE|unix.NLM_F_ACK), tc.handler.requests[0].flags)
// validate attribute(NFACCT_NAME)
assert.Equal(t, 1, len(tc.handler.requests[0].data))
assert.Equal(t,
tc.handler.requests[0].data[0].Serialize(),
nl.NewRtAttr(attrName, nl.ZeroTerminated(tc.counterName)).Serialize(),
)
}
})
}
}
func TestRunner_Get(t *testing.T) {
testCases := []struct {
name string
counterName string
counter *Counter
handler *fakeHandler
netlinkCalls int
err error
}{
{
name: "valid with padding",
counterName: "metric-1",
counter: &Counter{Name: "metric-1", Packets: 43214632547, Bytes: 2548697864523217},
handler: &fakeHandler{
responses: [][][]byte{{{
0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x01, 0x00,
0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x2d, 0x31,
0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x02, 0x00,
0x00, 0x00, 0x00, 0x0a, 0x0f, 0xca, 0xf6, 0x63,
0x0c, 0x00, 0x03, 0x00, 0x00, 0x09, 0x0e, 0x06,
0xf6, 0xda, 0xcd, 0xd1, 0x08, 0x00, 0x04, 0x00,
0x00, 0x00, 0x00, 0x01,
}}},
},
// expected calls: NFNL_MSG_ACCT_GET
netlinkCalls: 1,
},
{
name: "valid without padding",
counterName: "metrics",
counter: &Counter{Name: "metrics", Packets: 12, Bytes: 503},
handler: &fakeHandler{
responses: [][][]byte{{{
0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x01, 0x00,
0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x00,
0x0c, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x0c, 0x0c, 0x00, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xf7,
0x08, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x01,
}}},
},
// expected calls: NFNL_MSG_ACCT_GET
netlinkCalls: 1,
},
{
name: "missing netfilter generic header",
counterName: "metrics",
counter: nil,
handler: &fakeHandler{
responses: [][][]byte{{{
0x01, 0x00, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63,
0x73, 0x00, 0x0c, 0x00, 0x02, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x0c, 0x00,
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0xf7, 0x08, 0x00, 0x04, 0x00, 0x00, 0x00,
0x00, 0x01,
}}},
},
// expected calls: NFNL_MSG_ACCT_GET
netlinkCalls: 1,
err: ErrUnexpected,
},
{
name: "incorrect padding",
counterName: "metric-1",
counter: nil,
handler: &fakeHandler{
responses: [][][]byte{{{
0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x01, 0x00,
0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x2d, 0x31,
0x00, 0x0c, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
0x0a, 0x0f, 0xca, 0xf6, 0x63, 0x0c, 0x00, 0x03,
0x00, 0x00, 0x09, 0x0e, 0x06, 0xf6, 0xda, 0xcd,
0xd1, 0x08, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00,
0x01,
}}},
},
// expected calls: NFNL_MSG_ACCT_GET
netlinkCalls: 1,
err: ErrUnexpected,
},
{
name: "missing bytes attribute",
counterName: "metric-1",
counter: nil,
handler: &fakeHandler{
responses: [][][]byte{{{
0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x01, 0x00,
0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x2d, 0x31,
0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x02, 0x00,
0x00, 0x00, 0x00, 0x0a, 0x0f, 0xca, 0xf6, 0x63,
0x08, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x01,
}}},
},
// expected calls: NFNL_MSG_ACCT_GET
netlinkCalls: 1,
err: ErrUnexpected,
},
{
name: "missing packets attribute",
counterName: "metric-1",
counter: nil,
handler: &fakeHandler{
responses: [][][]byte{{{
0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x01, 0x00,
0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x2d, 0x31,
0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x03, 0x00,
0x00, 0x09, 0x0e, 0x06, 0xf6, 0xda, 0xcd, 0xd1,
0x08, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x01,
}}},
},
// expected calls: NFNL_MSG_ACCT_GET
netlinkCalls: 1,
err: ErrUnexpected,
},
{
name: "only name attribute",
counterName: "metric-1",
counter: nil,
handler: &fakeHandler{
responses: [][][]byte{{{
0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x01, 0x00,
0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x2d, 0x31,
0x00, 0x00, 0x00, 0x00,
}}},
},
// expected calls: NFNL_MSG_ACCT_GET
netlinkCalls: 1,
err: ErrUnexpected,
},
{
name: "get non-existent counter",
counterName: "metric-2",
handler: &fakeHandler{
errs: []error{syscall.ENOENT},
},
// expected calls: NFNL_MSG_ACCT_GET
netlinkCalls: 1,
err: ErrObjectNotFound,
},
{
name: "unexpected error",
counterName: "metric-2",
handler: &fakeHandler{
errs: []error{syscall.EMFILE},
},
// expected calls: NFNL_MSG_ACCT_GET
netlinkCalls: 1,
err: ErrUnexpected,
},
{
name: "exceeds max length",
counterName: "this-is-a-string-with-more-than-32-characters",
handler: &fakeHandler{},
// expected calls: zero (the error should be returned by this library)
netlinkCalls: 0,
err: ErrNameExceedsMaxLength,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
rnr, err := newInternal(tc.handler)
assert.NoError(t, err)
counter, err := rnr.Get(tc.counterName)
// validate number of requests
assert.Equal(t, tc.netlinkCalls, len(tc.handler.requests))
if tc.netlinkCalls > 0 {
// validate request
assert.Equal(t, cmdGet, tc.handler.requests[0].cmd)
assert.Equal(t, uint16(unix.NLM_F_REQUEST|unix.NLM_F_ACK), tc.handler.requests[0].flags)
// validate attribute(NFACCT_NAME)
assert.Equal(t, 1, len(tc.handler.requests[0].data))
assert.Equal(t,
tc.handler.requests[0].data[0].Serialize(),
nl.NewRtAttr(attrName, nl.ZeroTerminated(tc.counterName)).Serialize())
// validate response
if tc.err != nil {
assert.Nil(t, counter)
assert.ErrorContains(t, err, tc.err.Error())
} else {
assert.NotNil(t, counter)
assert.NoError(t, err)
assert.Equal(t, tc.counter.Name, counter.Name)
assert.Equal(t, tc.counter.Packets, counter.Packets)
assert.Equal(t, tc.counter.Bytes, counter.Bytes)
}
}
})
}
}
func TestRunner_Ensure(t *testing.T) {
testCases := []struct {
name string
counterName string
netlinkCalls int
handler *fakeHandler
}{
{
name: "counter doesnt exist",
counterName: "ct_established_accepted_packets",
handler: &fakeHandler{
errs: []error{syscall.ENOENT},
},
// expected calls - NFNL_MSG_ACCT_GET + NFNL_MSG_ACCT_NEW
netlinkCalls: 2,
},
{
name: "counter already exists",
counterName: "ct_invalid_dropped_packets",
handler: &fakeHandler{
responses: [][][]byte{{{
0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x01, 0x00,
0x63, 0x74, 0x5f, 0x69, 0x6e, 0x76, 0x61, 0x6c,
0x69, 0x64, 0x5f, 0x64, 0x72, 0x6f, 0x70, 0x70,
0x65, 0x64, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x65,
0x74, 0x73, 0x00, 0x00, 0x0c, 0x00, 0x02, 0x00,
0x00, 0x02, 0x68, 0xf3, 0x16, 0x58, 0x0e, 0x63,
0x0c, 0x00, 0x03, 0x00, 0x12, 0xc5, 0x37, 0xdf,
0xe5, 0xa1, 0xcd, 0xd1, 0x08, 0x00, 0x04, 0x00,
0x00, 0x00, 0x00, 0x01,
}}},
},
// expected calls - NFNL_MSG_ACCT_GET
netlinkCalls: 1,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
rnr, err := newInternal(tc.handler)
assert.NoError(t, err)
err = rnr.Ensure(tc.counterName)
assert.NoError(t, err)
// validate number of netlink requests
assert.Equal(t, tc.netlinkCalls, len(tc.handler.requests))
})
}
}
func TestRunner_List(t *testing.T) {
hndlr := &fakeHandler{
responses: [][][]byte{{
{
0x00, 0x00, 0x00, 0x00, 0x17, 0x00, 0x01, 0x00,
0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x2d, 0x74,
0x65, 0x73, 0x74, 0x2d, 0x6d, 0x65, 0x74, 0x72,
0x69, 0x63, 0x00, 0x00, 0x0c, 0x00, 0x02, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86,
0x0c, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x08, 0x60, 0x08, 0x00, 0x04, 0x00,
0x00, 0x00, 0x00, 0x01,
},
{
0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x01, 0x00,
0x6e, 0x66, 0x61, 0x63, 0x63, 0x74, 0x2d, 0x6c,
0x69, 0x73, 0x74, 0x2d, 0x74, 0x65, 0x73, 0x74,
0x2d, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x00,
0x0c, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x02, 0x0b, 0x96, 0x0c, 0x00, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0xe6, 0xc5, 0x74,
0x08, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x01,
},
{
0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x01, 0x00,
0x74, 0x65, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00,
0x0c, 0x00, 0x02, 0x00, 0x00, 0x00, 0x86, 0x8d,
0x44, 0xeb, 0xc7, 0x02, 0x0c, 0x00, 0x03, 0x00,
0x00, 0x6e, 0x5f, 0xe2, 0x89, 0x69, 0x3f, 0x9e,
0x08, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x01,
},
{
0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x01, 0x00,
0x63, 0x74, 0x5f, 0x69, 0x6e, 0x76, 0x61, 0x6c,
0x69, 0x64, 0x5f, 0x64, 0x72, 0x6f, 0x70, 0x70,
0x65, 0x64, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x65,
0x74, 0x73, 0x00, 0x00, 0x0c, 0x00, 0x02, 0x00,
0x00, 0x00, 0x01, 0x1e, 0x6e, 0xac, 0x20, 0xe9,
0x0c, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0d, 0x6d,
0x30, 0x11, 0x8a, 0xec, 0x08, 0x00, 0x04, 0x00,
0x00, 0x00, 0x00, 0x01,
},
}},
}
expected := []*Counter{
{Name: "random-test-metric", Packets: 134, Bytes: 2144},
{Name: "nfacct-list-test-metric", Packets: 134038, Bytes: 31901044},
{Name: "test", Packets: 147941304813314, Bytes: 31067674010795934},
{Name: "ct_invalid_dropped_packets", Packets: 1230217421033, Bytes: 14762609052396},
}
rnr, err := newInternal(hndlr)
assert.NoError(t, err)
counters, err := rnr.List()
// validate request(NFNL_MSG_ACCT_GET)
assert.Equal(t, 1, len(hndlr.requests))
assert.Equal(t, cmdGet, hndlr.requests[0].cmd)
assert.Equal(t, uint16(unix.NLM_F_REQUEST|unix.NLM_F_DUMP), hndlr.requests[0].flags)
// validate attributes
assert.Equal(t, 0, len(hndlr.requests[0].data))
// validate response
assert.NoError(t, err)
assert.NotNil(t, counters)
assert.Equal(t, len(expected), len(counters))
for i := 0; i < len(expected); i++ {
assert.Equal(t, expected[i].Name, counters[i].Name)
assert.Equal(t, expected[i].Packets, counters[i].Packets)
assert.Equal(t, expected[i].Bytes, counters[i].Bytes)
}
}
func TestDecode(t *testing.T) {
testCases := []struct {
name string
msg []byte
expected *Counter
}{
{
name: "valid",
msg: []byte{
0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x01, 0x00,
0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x2d, 0x31,
0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x02, 0x00,
0x00, 0x00, 0x00, 0x0a, 0x0f, 0xca, 0xf6, 0x63,
0x0c, 0x00, 0x03, 0x00, 0x00, 0x09, 0x0e, 0x06,
0xf6, 0xda, 0xcd, 0xd1, 0x08, 0x00, 0x04, 0x00,
0x00, 0x00, 0x00, 0x01,
},
expected: &Counter{Name: "metric-1", Packets: 43214632547, Bytes: 2548697864523217},
},
{
name: "attribute name missing",
msg: []byte{
0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x02, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0b, 0x96,
0x0c, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0xe6, 0xc5, 0x74, 0x08, 0x00, 0x04, 0x00,
0x00, 0x00, 0x00, 0x01,
},
expected: &Counter{Packets: 134038, Bytes: 31901044},
},
{
name: "attribute packets missing",
msg: []byte{
0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x01, 0x00,
0x63, 0x74, 0x5f, 0x69, 0x6e, 0x76, 0x61, 0x6c,
0x69, 0x64, 0x5f, 0x64, 0x72, 0x6f, 0x70, 0x70,
0x65, 0x64, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x65,
0x74, 0x73, 0x00, 0x00, 0x0c, 0x00, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x60,
0x08, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x01,
},
expected: &Counter{Name: "ct_invalid_dropped_packets", Bytes: 2144},
},
{
name: "attribute bytes missing",
msg: []byte{
0x00, 0x00, 0x00, 0x00, 0x17, 0x00, 0x01, 0x00,
0x72, 0x61, 0x6e, 0x64, 0x6f, 0x6d, 0x2d, 0x74,
0x65, 0x73, 0x74, 0x2d, 0x6d, 0x65, 0x74, 0x72,
0x69, 0x63, 0x00, 0x00, 0x0c, 0x00, 0x02, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xf7,
},
expected: &Counter{Name: "random-test-metric", Packets: 503},
},
{
name: "attribute packets and bytes missing",
msg: []byte{
0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x01, 0x00,
0x74, 0x65, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00,
},
expected: &Counter{Name: "test"},
},
{
name: "only netfilter generic header present",
msg: []byte{
0x00, 0x00, 0x00, 0x00,
},
expected: &Counter{},
},
{
name: "only packets attribute",
msg: []byte{
0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x02, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0b, 0x96,
},
expected: &Counter{Packets: 134038},
},
{
name: "only bytes attribute",
msg: []byte{
0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c,
},
expected: &Counter{Bytes: 12},
},
{
name: "new attribute in the beginning",
msg: []byte{
0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, 0x0d, 0x00, 0x01, 0x00,
0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x2d, 0x31,
0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x02, 0x00,
0x00, 0x00, 0x00, 0x0a, 0x0f, 0xca, 0xf6, 0x63,
0x0c, 0x00, 0x03, 0x00, 0x00, 0x09, 0x0e, 0x06,
0xf6, 0xda, 0xcd, 0xd1, 0x08, 0x00, 0x04, 0x00,
0x00, 0x00, 0x00, 0x01,
},
expected: &Counter{Name: "metric-1", Packets: 43214632547, Bytes: 2548697864523217},
},
{
name: "new attribute in the end",
msg: []byte{
0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, 0x0d, 0x00, 0x01, 0x00,
0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x2d, 0x31,
0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x02, 0x00,
0x00, 0x00, 0x00, 0x0a, 0x0f, 0xca, 0xf6, 0x63,
0x0c, 0x00, 0x03, 0x00, 0x00, 0x09, 0x0e, 0x06,
0xf6, 0xda, 0xcd, 0xd1, 0x08, 0x00, 0x04, 0x00,
0x00, 0x00, 0x00, 0x01, 0x0c, 0x00, 0x00, 0x01,
0x02, 0x03, 0x0e, 0x3f, 0xf6, 0xda, 0xcd, 0xd1,
},
expected: &Counter{Name: "metric-1", Packets: 43214632547, Bytes: 2548697864523217},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
counter, err := decode(tc.msg, false)
assert.NoError(t, err)
assert.NotNil(t, counter)
assert.Equal(t, tc.expected.Name, counter.Name)
assert.Equal(t, tc.expected.Packets, counter.Packets)
assert.Equal(t, tc.expected.Bytes, counter.Bytes)
})
}
}

View File

@ -0,0 +1,31 @@
//go:build !linux
// +build !linux
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package nfacct
import (
"fmt"
"runtime"
)
var unsupportedError = fmt.Errorf(runtime.GOOS + "/" + runtime.GOARCH + "is unsupported")
func New() (Interface, error) {
return nil, unsupportedError
}