kubernetes/pkg/proxy/util/nfacct/nfacct_linux_test.go

//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.Len(t, tc.handler.requests, tc.netlinkCalls)

			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.Len(t, tc.handler.requests[0].data, 1)
				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.Len(t, tc.handler.requests, tc.netlinkCalls)
			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.Len(t, tc.handler.requests[0].data, 1)
				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.Len(t, tc.handler.requests, tc.netlinkCalls)
		})
	}

}

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.Len(t, hndlr.requests, 1)
	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.Empty(t, 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)
		})
	}
}