Skip to content

netlink: add struct tag-based attribute codec#287

Draft
nickgarlis wants to merge 1 commit intomainfrom
add-attr-codec
Draft

netlink: add struct tag-based attribute codec#287
nickgarlis wants to merge 1 commit intomainfrom
add-attr-codec

Conversation

@nickgarlis
Copy link
Copy Markdown
Collaborator

Add a reflection-based Codec that maps struct fields tagged with netlink:"<index>[,opts]" to netlink attributes, providing a declarative alternative to manual AttributeEncoder/AttributeDecoder usage.

Supported types: scalars (uint*/int* with optional be/le byte order), strings, []byte, bools (flags), nested structs, pointers (optional), slices (multi/indexed layouts), and sub-messages via resolver functions.

Struct metadata is cached per type to avoid repeated reflection overhead.

See #286.

Add a reflection-based Codec that maps struct fields tagged with
`netlink:"<index>[,opts]"` to netlink attributes, providing a
declarative alternative to manual AttributeEncoder/AttributeDecoder
usage.

Supported types: scalars (uint*/int* with optional be/le byte order),
strings, []byte, bools (flags), nested structs, pointers (optional),
slices (multi/indexed layouts), and sub-messages via resolver functions.

Struct metadata is cached per type to avoid repeated reflection
overhead.

See #286.
@nickgarlis
Copy link
Copy Markdown
Collaborator Author

Opinions are welcome. I am not sure whether it's a good idea to add another attribute encoder to the package but it could simplify things a lot by reducing boilerplate.

Tagging a few people here who might be interested: @aojea @SuperQ @florianl

@aojea
Copy link
Copy Markdown
Contributor

aojea commented Apr 17, 2026

what will be the use cases for this? can you provide some realistic ones?

@nickgarlis
Copy link
Copy Markdown
Collaborator Author

nickgarlis commented Apr 17, 2026

what will be the use cases for this? can you provide some realistic ones?

Taking nftables as an example, for each attribute set you end up manually writing serialization and deserialization, which adds up to a lot of boilerplate. A typical table encode/decode looks something like this:

func tableFromAttrs(ad *netlink.AttributeDecoder) Table {
    var t Table
    ad.ByteOrder = binary.BigEndian
    for ad.Next() {
        switch ad.Type() {
        case unix.NFTA_TABLE_NAME:
            t.Name = ad.String()
        case unix.NFTA_TABLE_FLAGS:
            t.Flags = ad.Uint32()
        case unix.NFTA_TABLE_USE:
            v := ad.Uint32()
            t.Use = &v
        case unix.NFTA_TABLE_HANDLE:
            v := ad.Uint64()
            t.Handle = &v
        }
    }
    return t
}

func tableToAttrs(t *Table) ([]byte, error) {
    ae := netlink.NewAttributeEncoder()
    ae.ByteOrder = binary.BigEndian
    ae.String(unix.NFTA_TABLE_NAME, t.Name)
    ae.Uint32(unix.NFTA_TABLE_FLAGS, t.Flags)
    if t.Use != nil {
        ae.Uint32(unix.NFTA_TABLE_USE, *t.Use)
    }
    if t.Handle != nil {
        ae.Uint64(unix.NFTA_TABLE_HANDLE, *t.Handle)
    }
    return ae.Encode()
}

With a tag-based codec, the struct definition becomes the single source of truth similar to how encoding/json or gopkg.in/yaml.v3 work:

type Table struct {
    Name   string  `netlink:"1,be"`  // NFTA_TABLE_NAME
    Flags  uint32  `netlink:"2,be"`  // NFTA_TABLE_FLAGS
    Use    *uint32 `netlink:"3,be"`  // NFTA_TABLE_USE
    Handle *uint64 `netlink:"4,be"`  // NFTA_TABLE_HANDLE
}

codec := netlink.NewAttributeCodec(nil)

// encode
b, err := codec.Marshal(t)

// decode
var t Table
err := codec.Unmarshal(b, &t)

I started exploring this while building a YNL code generator for Go. In YNL, operations often use subsets of a shared attribute set. For example, newtable, gettable, deltable, and destroytable all reference table-attrs but each uses different fields. A naive generator would emit a separate struct (plus encode/decode boilerplate) for every operation/direction combination, which quickly explodes in families like nftables with dozens of operations. With a tag-based codec, the generator can instead emit one struct per attribute set using pointer fields for optional attributes, and get serialization for free.

@nickgarlis
Copy link
Copy Markdown
Collaborator Author

nickgarlis commented Apr 17, 2026

Here’s an example of a more advanced case where discrimination is needed. The tag options selector and sub-msg follow the YNL spec: https://elixir.bootlin.com/linux/v7.0/source/Documentation/netlink/specs/nftables.yaml#L433

type Expression interface {
    exprType() string
}

type ExprMeta struct {
    Dreg *uint32 `netlink:"1,be"`
    Key  uint32  `netlink:"2,be"`
    Sreg *uint32 `netlink:"3,be"`
}

func (*ExprMeta) exprType() string { return "meta" }

type ExprCounter struct {
    Bytes   uint64 `netlink:"1,be"`
    Packets uint64 `netlink:"2,be"`
}

func (*ExprCounter) exprType() string { return "counter" }

// "Data" is resolved based the "Name" field.
type ExprAttrs struct {
    Name string     `netlink:"1,be"`
    Data Expression `netlink:"2,submsg=expr-ops,selector=Name"`
}

type RuleAttrs struct {
    Table string      `netlink:"1,be"`
    Chain string      `netlink:"2,be"`
    Exprs []ExprAttrs `netlink:"4,indexed"`
}

codec := netlink.NewAttributeCodec(&netlink.AttributeCodecConfig{
    Resolvers: map[string]netlink.SubMessageResolver{
        "expr-ops": func(selector any) (any, error) {
            switch selector.(string) {
            case "meta":
                return &ExprMeta{}, nil
            case "counter":
                return &ExprCounter{}, nil
            default:
                return nil, fmt.Errorf("unknown expression: %s", selector)
            }
        },
    },
})

rule := RuleAttrs{
    Table: "filter",
    Chain: "input",
    Exprs: []ExprAttrs{
        {Name: "meta", Data: &ExprMeta{Key: 16, Dreg: ptr(uint32(1))}},
        // "cmp" expression omitted for brevity
        {Name: "counter", Data: &ExprCounter{}},
    },
}

b, err := codec.Marshal(rule)

var got RuleAttrs
err = codec.Unmarshal(b, &got)
// got.Exprs[0].Data.(*ExprMeta).Key == 16
// got.Exprs[1].Data.(*ExprCounter) != nil

Comment thread attribute_codec.go

// Unmarshal decodes netlink attribute bytes into a struct.
func (c *Codec) Unmarshal(b []byte, v any) error {
rv := reflect.ValueOf(v)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generally avoid functions like reflect.ValueOf() because they hinder the Go compiler's ability to perform dead code elimination. This can lead to a significant and unnecessary increase in the final binary size. While hand-writing logic for known structs is more verbose, the resulting performance and footprint optimizations are well worth the manual effort.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for having a look! Yeah I am not too keen on it being slower. Admittedly, I hadn't considered the binary size consequences. The idea was that it might be useful for use cases where the performance tradeoffs are acceptable. If there are no such use cases then having and maintaining this feature is probably not worth it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants