diff --git a/go.mod b/go.mod index 0b6d332dfc1..51e191d562d 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/pkg/proxy/util/nfacct/handler.go b/pkg/proxy/util/nfacct/handler.go new file mode 100644 index 00000000000..ec14ea41833 --- /dev/null +++ b/pkg/proxy/util/nfacct/handler.go @@ -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 +} diff --git a/pkg/proxy/util/nfacct/nfacct.go b/pkg/proxy/util/nfacct/nfacct.go new file mode 100644 index 00000000000..cdb7ef4cc7c --- /dev/null +++ b/pkg/proxy/util/nfacct/nfacct.go @@ -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) +} diff --git a/pkg/proxy/util/nfacct/nfacct_linux.go b/pkg/proxy/util/nfacct/nfacct_linux.go new file mode 100644 index 00000000000..ad0cfe03197 --- /dev/null +++ b/pkg/proxy/util/nfacct/nfacct_linux.go @@ -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 +} diff --git a/pkg/proxy/util/nfacct/nfacct_linux_test.go b/pkg/proxy/util/nfacct/nfacct_linux_test.go new file mode 100644 index 00000000000..7d721221320 --- /dev/null +++ b/pkg/proxy/util/nfacct/nfacct_linux_test.go @@ -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) + }) + } +} diff --git a/pkg/proxy/util/nfacct/nfacct_others.go b/pkg/proxy/util/nfacct/nfacct_others.go new file mode 100644 index 00000000000..29422328dfb --- /dev/null +++ b/pkg/proxy/util/nfacct/nfacct_others.go @@ -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 +}