Repository: google/nftables

Issue:
feature: add support for table flags
Hi!

Currently the table flags are not used:

https://github.com/google/nftables/blob/68e1406c13281ebc65b8cb5733ee5882244809d5/table.go#L58

Supported flags are doormant (0x1), owner (0x2) and persist (0x4): https://manpages.debian.org/testing/nftables/nft.8.en.html

https://lwn.net/Articles/857369/

Defined flags in kernel: 
```
enum nft_table_flags {
	NFT_TABLE_F_DORMANT	= 0x1,
	NFT_TABLE_F_OWNER	= 0x2,
};
```

https://git.netfilter.org/nftables/tree/include/linux/netfilter/nf_tables.h

```
~ # nft -d mnl add table inet dormant-tbl "{ flags dormant; }"
(...)
----------------	------------------
| 01 00 00 00  |	|  extra header  |
|00016|--|00001|	|len |flags| type|
| 64 6f 72 6d  |	|      data      |	 d o r m
| 61 6e 74 2d  |	|      data      |	 a n t -
| 74 62 6c 00  |	|      data      |	 t b l  
|00008|--|00002|	|len |flags| type|
| 00 00 00 01  |	|      data      |	        
----------------	------------------
```


```
~ # nft -d mnl add table inet persist-tbl "{ flags persist; }"
(...)
----------------	------------------
| 01 00 00 00  |	|  extra header  |
|00016|--|00001|	|len |flags| type|
| 70 65 72 73  |	|      data      |	 p e r s
| 69 73 74 2d  |	|      data      |	 i s t -
| 74 62 6c 00  |	|      data      |	 t b l  
|00008|--|00002|	|len |flags| type|
| 00 00 00 04  |	|      data      |	        
----------------	------------------
```

```
# this feature allows a process to own a table in exclusivity, so we do it interactively from nft

~ # nft -d mnl -i 
nft> add table ip owner-tbl "{ flags owner; }"
(...)
----------------	------------------
| 02 00 00 00  |	|  extra header  |
|00014|--|00001|	|len |flags| type|
| 6f 77 6e 65  |	|      data      |	 o w n e
| 72 2d 74 62  |	|      data      |	 r - t b
| 6c 00 00 00  |	|      data      |	 l      
|00008|--|00002|	|len |flags| type|
| 00 00 00 02  |	|      data      |	        
----------------	------------------
```

```
~ # nft -d mnl -i 
nft> add table inet ownpers-tbl "{ flags owner, persist; }"
(...)
----------------	------------------
| 01 00 00 00  |	|  extra header  |
|00016|--|00001|	|len |flags| type|
| 6f 77 6e 70  |	|      data      |	 o w n p
| 65 72 73 2d  |	|      data      |	 e r s -
| 74 62 6c 00  |	|      data      |	 t b l  
|00008|--|00002|	|len |flags| type|
| 00 00 00 06  |	|      data      |	        
----------------	------------------

```


Hints from issue comments:
@stapelberg: OK. If you want to see this supported, please contribute a pull request with the required changes. Thanks!@gustavo-iniguez-goya: (Just in case anyone is interested in this feature, I'm documenting what I've observed.)

The PID of the owner is sent in a NEWGEN message, which the kernel doesn't seem to like if it's sent wrapped in NFNL_MSG_BATCH_BEGIN ... NFNL_MSG_BATCH_END (because of conn.Flush() -> batch()).

This would require changing how Flush() works, or create another Flushxxx() to send the messages one by one, or maybe gen.FlushNewGen() to just send the newgen message. So since I don't know what the correct approach is, I'm not submitting a PR for now. I haven't looked into parsing the table flags yet either.

Valid use:
```
~ # strace -f -v -x -s 2048 -eraw=sendto nft add table ip xxx "{ flags owner; }"

recvmsg(3, {msg_name={sa_family=AF_NETLINK, nl_pid=0, nl_groups=00000000}, msg_namelen=12, msg_iov=[{iov_base=[{nlmsg_len=44, nlmsg_type=NFNL_SUBSYS_NFTABLES<<8|NFT_MSG_NEWGEN, nlmsg_flags=0, nlmsg_seq=0, nlmsg_pid=15286}, {nfgen_family=AF_UNSPEC, version=NFNETLINK_V0, res_id=htons(50)}, [[{nla_len=8, nla_type=0x1}, "\x00\x00\x00\x32"], [{nla_len=8, nla_type=0x2}, "\x00\x00\x3b\xb6"], [{nla_len=8, nla_type=0x3}, "\x6e\x66\x74\x00"]]], iov_len=69631}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, 0) = 44


0x2 -> PID -> "\x00\x00\x3b\xb6"
0x3 -> Comm -> "\x6e\x66\x74\x00" (nft)

(...)

recvmsg(3, {msg_name={sa_family=AF_NETLINK, nl_pid=0, nl_groups=00000000}, msg_namelen=12, msg_iov=[{iov_base=[[{nlmsg_len=60, nlmsg_type=NFNL_SUBSYS_NFTABLES<<8|NFT_MSG_NEWTABLE, nlmsg_flags=NLM_F_MULTI, nlmsg_seq=0, nlmsg_pid=15286}, {nfgen_family=AF_UNIX, version=NFNETLINK_V0, res_id=htons(50)}, [[{nla_len=11, nla_type=0x1}, "\x66\x69\x6c\x74\x65\x72\x00"], [{nla_len=8, nla_type=0x3}, "\x00\x00\x00\x03"], [{nla_len=12, nla_type=0x4}, "\x00\x00\x00\x00\x00\x00\x00\x01"], [{nla_len=8, nla_type=0x2}, "\x00\x00\x00\x00"]]], [{nlmsg_len=56, nlmsg_type=NFNL_SUBSYS_NFTABLES<<8|NFT_MSG_NEWTABLE, nlmsg_flags=NLM_F_MULTI, nlmsg_seq=0, nlmsg_pid=15286}, {nfgen_family=AF_UNIX, version=NFNETLINK_V0, res_id=htons(50)}, [[{nla_len=8, nla_type=0x1}, "\x6e\x61\x74\x00"], [{nla_len=8, nla_type=0x3}, "\x00\x00\x00\x05"], [{nla_len=12, nla_type=0x4}, "\x00\x00\x00\x00\x00\x00\x00\x02"], [{nla_len=8, nla_type=0x2}, "\x00\x00\x00\x00"]]], [{nlmsg_len=60, nlmsg_type=NFNL_SUBSYS_NFTABLES<<8|NFT_MSG_NEWTABLE, nlmsg_flags=NLM_F_MULTI, nlmsg_seq=0, nlmsg_pid=15286}, {nfgen_family=AF_UNIX, version=NFNETLINK_V0, res_id=htons(50)}, [[{nla_len=11, nla_type=0x1}, "\x6d\x61\x6e\x67\x6c\x65\x00"], [{nla_len=8, nla_type=0x3}, "\x00\x00\x00\x04"], [{nla_len=12, nla_type=0x4}, "\x00\x00\x00\x00\x00\x00\x00\x03"], [{nla_len=8, nla_type=0x2}, "\x00\x00\x00\x00"]]]], iov_len=69631}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, 0) = 176

```

Invalid use, sending the NEWGEN msg wrapped in batch messages:

```
sendmsg(3, {msg_name={sa_family=AF_NETLINK, nl_pid=0, nl_groups=00000000}, msg_namelen=12, msg_iov=[{iov_base=[[{nlmsg_len=20, nlmsg_type=NFNL_MSG_BATCH_BEGIN, nlmsg_flags=NLM_F_REQUEST, nlmsg_seq=1633674086, nlmsg_pid=15235}, {nfgen_family=AF_UNSPEC, version=NFNETLINK_V0, res_id=htons(10)}], [{nlmsg_len=44, nlmsg_type=NFNL_SUBSYS_NFTABLES<<8|NFT_MSG_NEWGEN, nlmsg_flags=0, nlmsg_seq=1633674087, nlmsg_pid=15235}, {nfgen_family=AF_UNSPEC, version=NFNETLINK_V0, res_id=htons(1)}, [[{nla_len=8, nla_type=0x1}, "\x00\x00\x00\x01"], [{nla_len=8, nla_type=0x2}, "\x00\x00\x3b\x83"], [{nla_len=8, nla_type=0x3}, "\x74\x62\x6c\x00"]]], [{nlmsg_len=36, nlmsg_type=NFNL_SUBSYS_NFTABLES<<8|NFT_MSG_NEWTABLE, nlmsg_flags=NLM_F_REQUEST|NLM_F_ACK|NLM_F_CREATE, nlmsg_seq=1633674088, nlmsg_pid=15235}, {nfgen_family=AF_INET, version=NFNETLINK_V0, res_id=htons(0)}, [[{nla_len=8, nla_type=0x1}, "\x75\x75\x75\x00"], [{nla_len=8, nla_type=0x2}, "\x00\x00\x00\x02"]]], [{nlmsg_len=20, nlmsg_type=NFNL_MSG_BATCH_END, nlmsg_flags=NLM_F_REQUEST, nlmsg_seq=1633674089, nlmsg_pid=15235}, {nfgen_family=AF_UNSPEC, version=NFNETLINK_V0, res_id=htons(10)}]], iov_len=120}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, 0

(...)

recvmsg resumed>{msg_name={sa_family=AF_NETLINK, nl_pid=0, nl_groups=00000000}, msg_namelen=112 => 12, msg_iov=[{iov_base=[{nlmsg_len=64, nlmsg_type=NLMSG_ERROR, nlmsg_flags=0, nlmsg_seq=1633674087, nlmsg_pid=15235}, {error=-EINVAL, msg=[{nlmsg_len=44, nlmsg_type=NFNL_SUBSYS_NFTABLES<<8|NFT_MSG_NEWGEN, nlmsg_flags=0, nlmsg_seq=1633674087, nlmsg_pid=15235}, {nfgen_family=AF_UNSPEC, version=NFNETLINK_V0, res_id=htons(1)}, [[{nla_len=8, nla_type=0x1}, "\x00\x00\x00\x01"], [{nla_len=8, nla_type=0x2}, "\x00\x00\x3b\x83"], [{nla_len=8, nla_type=0x3}, "\x74\x62\x6c\x00"]]]}], iov_len=4096}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, MSG_PEEK) = 64

(error -> conn.Receive: netlink receive: invalid argument)
```

<details>
  <summary>PoC</summary>

```Golang
package main

import (
    "fmt"
    "log"
    "os"
    "time"

    "github.com/google/nftables"
)

func main() {
    fmt.Println("pid:", os.Getpid())

    c, err := nftables.New(nftables.AsLasting())
    defer func() {
        if err := c.CloseLasting(); err != nil {
            log.Fatal(err)
        }
    }()
    if err != nil {
        log.Fatal(err)
    }

    table := &nftables.Table{
        Family:   nftables.TableFamilyIPv4,
        Name:     "uuu",
        Flags:    nftables.TableFlagOwner,
        PidOwner: uint32(os.Getpid()),
        PidComm:  "tbl", // go build -o tbl .
    }

    // firstly send the NEWGEN msg
    c.AddTableOwner(table)
    if err := c.FlushNewGen(); err != nil {
        log.Fatal(err.Error())
    }

    // then send the rest of the messages
    c.AddTable(table)
    if err := c.Flush(); err != nil {
        log.Fatal(err.Error())
    }
    fmt.Println("table created")

    // don't exit, in order to verify that the table has been added while the pid is alive.
    for i := 0; i < 120; i++ {
        time.Sleep(1 * time.Second)
    }
}
```

```
diff --git a/gen.go b/gen.go
index 0d4580d..2bea8db 100644
--- a/gen.go
+++ b/gen.go
@@ -4,6 +4,7 @@ import (
        "encoding/binary"
        "fmt"
 
+       "github.com/google/nftables/binaryutil"
        "github.com/mdlayher/netlink"
        "golang.org/x/sys/unix"
 )
@@ -16,6 +17,52 @@ type GenMsg struct {
 
 const genHeaderType = netlink.HeaderType((unix.NFNL_SUBSYS_NFTABLES << 8) | unix.NFT_MSG_NEWGEN)
 
+func (cc *Conn) FlushNewGen() error {
+       cc.mu.Lock()
+
+       if len(cc.messages) == 0 {
+               return fmt.Errorf("FlushNewGen: no messages")
+       }
+
+       defer func() {
+               cc.messages = nil
+               cc.allocatedIDs = 0
+               cc.mu.Unlock()
+       }()
+
+       conn, closer, err := cc.netlinkConnUnderLock()
+       if err != nil {
+               return err
+       }
+       defer func() { _ = closer() }()
+
+       nMsg := netlink.Message{
+               Header: cc.messages[0].Header,
+               Data:   cc.messages[0].Data,
+       }
+
+       _, err = conn.Send(nMsg)
+       if err != nil {
+               return fmt.Errorf("FlushNewGen send: %w", err)
+       }
+
+       return nil
+}
+
+func (cc *Conn) NewGenMsg(reqID, pid uint32, comm string) netlinkMessage {
+       flags_data := cc.marshalAttr([]netlink.Attribute{
+               {Type: unix.NFTA_GEN_ID, Data: binaryutil.BigEndian.PutUint32(reqID)},
+               {Type: unix.NFTA_GEN_PROC_PID, Data: binaryutil.BigEndian.PutUint32(pid)},
+               {Type: unix.NFTA_GEN_PROC_NAME, Data: []byte(comm + "\x00")},
+       })
+       return netlinkMessage{
+               Header: netlink.Header{
+                       Type: genHeaderType,
+               },
+               Data: append(extraHeader(unix.AF_UNSPEC, uint16(reqID)), flags_data...),
+       }
+}
+
 func genFromMsg(msg netlink.Message) (*GenMsg, error) {
        if got, want := msg.Header.Type, genHeaderType; got != want {
                return nil, fmt.Errorf("unexpected header type: got %v, want %v", got, want)
diff --git a/nftables_test.go b/nftables_test.go
index fd0fa1a..f188d25 100644
--- a/nftables_test.go
+++ b/nftables_test.go
@@ -2121,6 +2121,107 @@ func TestListChainByNameUsingLasting(t *testing.T) {
        }
 }
 
+func TestAddTableFlags(t *testing.T) {
+       tests := []struct {
+               name string
+               tbl  *nftables.Table
+               want [][]byte
+       }{
+               {
+                       name: "tbl-dormant",
+                       tbl: &nftables.Table{
+                               Family: nftables.TableFamilyIPv4,
+                               Name:   "tbl-dormant",
+                               Flags:  nftables.TableFlagDormant,
+                       },
+                       want: [][]byte{
+                               // batch begin
+                               []byte("\x00\x00\x00\x0a"),
+                               // nft add table ip tbl-dormant "{ flags dormant; }"
+                               []byte("\x02\x00\x00\x00\x10\x00\x01\x00\x74\x62\x6c\x2d\x64\x6f\x72\x6d\x61\x6e\x74\x00\x08\x00\x02\x00\x00\x00\x00\x01"),
+
+                               // batch end
+                               []byte("\x00\x00\x00\x0a"),
+                       },
+               },
+               {
+                       name: "tbl-owner",
+                       tbl: &nftables.Table{
+                               Family: nftables.TableFamilyIPv4,
+                               Name:   "tbl-owner",
+                               Flags:  nftables.TableFlagOwner,
+                       },
+                       want: [][]byte{
+                               // batch begin
+                               []byte("\x00\x00\x00\x0a"),
+                               // nft add table ip tbl-owner "{ flags owner; }"
+                               []byte("\x02\x00\x00\x00\x0e\x00\x01\x00\x74\x62\x6c\x2d\x6f\x77\x6e\x65\x72\x00\x00\x00\x08\x00\x02\x00\x00\x00\x00\x02"),
+                               // batch end
+                               []byte("\x00\x00\x00\x0a"),
+                       },
+               },
+               {
+                       name: "tbl-owner-persist",
+                       tbl: &nftables.Table{
+                               Family: nftables.TableFamilyIPv4,
+                               Name:   "tbl-owner-persist",
+                               Flags:  nftables.TableFlagOwner | nftables.TableFlagPersist,
+                       },
+                       want: [][]byte{
+                               // batch begin
+                               []byte("\x00\x00\x00\x0a"),
+                               // nft add table ip tbl-owner-persist "{ flags owner,persist; }"
+                               []byte("\x02\x00\x00\x00\x16\x00\x01\x00\x74\x62\x6c\x2d\x6f\x77\x6e\x65\x72\x2d\x70\x65\x72\x73\x69\x73\x74\x00\x00\x00\x08\x00\x02\x00\x00\x00\x00\x06"),
+                               // batch end
+                               []byte("\x00\x00\x00\x0a"),
+                       },
+               },
+               {
+                       name: "tbl-persist",
+                       tbl: &nftables.Table{
+                               Family: nftables.TableFamilyIPv4,
+                               Name:   "tbl-persist",
+                               Flags:  nftables.TableFlagPersist,
+                       },
+                       want: [][]byte{
+                               // batch begin
+                               []byte("\x00\x00\x00\x0a"),
+                               // nft add table ip tbl-persist "{ flags persist; }"
+                               []byte("\x02\x00\x00\x00\x10\x00\x01\x00\x74\x62\x6c\x2d\x70\x65\x72\x73\x69\x73\x74\x00\x08\x00\x02\x00\x00\x00\x00\x04"),
+                               // batch end
+                               []byte("\x00\x00\x00\x0a"),
+                       },
+               },
+               {
+                       name: "tbl-dormant-persist",
+                       tbl: &nftables.Table{
+                               Family: nftables.TableFamilyIPv4,
+                               Name:   "tbl-dormant-persist",
+                               Flags:  nftables.TableFlagDormant | nftables.TableFlagPersist,
+                       },
+                       want: [][]byte{
+                               // batch begin
+                               []byte("\x00\x00\x00\x0a"),
+                               // nft add table ip tbl-dormant-persist "{ flags dormant,persist; }"
+                               []byte("\x02\x00\x00\x00\x18\x00\x01\x00\x74\x62\x6c\x2d\x64\x6f\x72\x6d\x61\x6e\x74\x2d\x70\x65\x72\x73\x69\x73\x74\x00\x08\x00\x02\x00\x00\x00\x00\x05"),
+                               // batch end
+                               []byte("\x00\x00\x00\x0a"),
+                       },
+               },
+       }
+       for _, tt := range tests {
+               c, err := nftables.New(expectMessages(t, tt.want))
+               if err != nil {
+                       t.Fatal(err)
+               }
+
+               c.AddTable(tt.tbl)
+               if err := c.Flush(); err != nil {
+                       t.Errorf("conn.Flush() failed: %v", err)
+               }
+       }
+}
+
 func TestListTableByName(t *testing.T) {
        conn, newNS := nftest.OpenSystemConn(t, *enableSysTests)
        defer nftest.CleanupSystemConn(t, newNS)
diff --git a/table.go b/table.go
index 3686b7a..036c75e 100644
--- a/table.go
+++ b/table.go
@@ -17,6 +17,7 @@ package nftables
 import (
        "fmt"
 
+       "github.com/google/nftables/binaryutil"
        "github.com/mdlayher/netlink"
        "golang.org/x/sys/unix"
 )
@@ -40,13 +41,21 @@ const (
        TableFamilyBridge      TableFamily = unix.NFPROTO_BRIDGE
 )
 
+const (
+       TableFlagDormant = unix.NFT_TABLE_F_DORMANT
+       TableFlagOwner   = 0x2
+       TableFlagPersist = 0x4
+)
+
 // A Table contains Chains. See also
 // https://wiki.nftables.org/wiki-nftables/index.php/Configuring_tables
 type Table struct {
-       Name   string // NFTA_TABLE_NAME
-       Use    uint32 // NFTA_TABLE_USE (Number of chains in table)
-       Flags  uint32 // NFTA_TABLE_FLAGS
-       Family TableFamily
+       Name     string // NFTA_TABLE_NAME
+       PidComm  string // NFTA_PROC_NAME
+       PidOwner uint32 // NFTA_PROC_PID
+       Use      uint32 // NFTA_TABLE_USE (Number of chains in table)
+       Flags    uint32 // NFTA_TABLE_FLAGS
+       Family   TableFamily
 }
 
 // DelTable deletes a specific table, along with all chains/rules it contains.
@@ -71,7 +80,7 @@ func (cc *Conn) addTable(t *Table, flag netlink.HeaderFlags) *Table {
        defer cc.mu.Unlock()
        data := cc.marshalAttr([]netlink.Attribute{
                {Type: unix.NFTA_TABLE_NAME, Data: []byte(t.Name + "\x00")},
-               {Type: unix.NFTA_TABLE_FLAGS, Data: []byte{0, 0, 0, 0}},
+               {Type: unix.NFTA_TABLE_FLAGS, Data: binaryutil.BigEndian.PutUint32(t.Flags)},
        })
        cc.messages = append(cc.messages, netlinkMessage{
                Header: netlink.Header{
@@ -89,6 +98,15 @@ func (cc *Conn) AddTable(t *Table) *Table {
        return cc.addTable(t, netlink.Create)
 }
 
+func (cc *Conn) AddTableOwner(t *Table) {
+       if t.PidOwner <= 0 {
+               return
+       }
+       reqID := cc.allocateTransactionID()
+       nlMsg := cc.NewGenMsg(reqID, t.PidOwner, t.PidComm)
+       cc.messages = append(cc.messages, nlMsg)
+}
+
 // CreateTable create the specified Table if it do not existed.
 // just like `nft create table ...`.
 func (cc *Conn) CreateTable(t *Table) *Table {
```
</details>@gustavo-iniguez-goya: @nickgarlis thank youu!!! :)

Solution patch:
diff --git a/conn.go b/conn.go
index d785196..8e3a684 100644
--- a/conn.go
+++ b/conn.go
@@ -552,3 +552,23 @@ func (cc *Conn) enlargeReadBuffer(conn *netlink.Conn) error {
 	}
 	return nil
 }
+
+// getPortIDUnderLock returns the netlink port ID associated with this
+// connection. It must be called while holding the Conn.mu lock.
+func (cc *Conn) getPortIDUnderLock() (uint32, error) {
+	conn, closer, err := cc.netlinkConnUnderLock()
+	if err != nil {
+		return 0, err
+	}
+	defer func() { _ = closer() }()
+
+	return conn.PID(), nil
+}
+
+// GetPortID returns the netlink port ID associated with this connection.
+func (cc *Conn) GetPortID() (uint32, error) {
+	cc.mu.Lock()
+	defer cc.mu.Unlock()
+	pid, err := cc.getPortIDUnderLock()
+	return pid, err
+}
diff --git a/go.mod b/go.mod
index 7f1707f..c862aa1 100644
--- a/go.mod
+++ b/go.mod
@@ -3,15 +3,15 @@ module github.com/google/nftables
 go 1.23.0
 
 require (
-	github.com/google/go-cmp v0.6.0
-	github.com/mdlayher/netlink v1.7.3-0.20250702063131-0f7746f74615
+	github.com/google/go-cmp v0.7.0
+	github.com/mdlayher/netlink v1.8.1-0.20251028132421-dcc6cab9a6eb
 	github.com/vishvananda/netlink v1.3.0
 	github.com/vishvananda/netns v0.0.4
-	golang.org/x/sys v0.31.0
+	golang.org/x/sys v0.35.0
 )
 
 require (
 	github.com/mdlayher/socket v0.5.1 // indirect
-	golang.org/x/net v0.38.0 // indirect
+	golang.org/x/net v0.43.0 // indirect
 	golang.org/x/sync v0.6.0 // indirect
 )
diff --git a/go.sum b/go.sum
index ec8b934..a61228b 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,11 @@
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/mdlayher/netlink v1.7.3-0.20250702063131-0f7746f74615 h1:5T2ai+PpYFKe+tyNj/ZxePZGiYoG5xDOylT30nywJUU=
 github.com/mdlayher/netlink v1.7.3-0.20250702063131-0f7746f74615/go.mod h1:ZlWrPUV9wyD64k5skWrIv4WDQmmiUbkNnCkBtEWYKwU=
+github.com/mdlayher/netlink v1.8.1-0.20251028132421-dcc6cab9a6eb h1:DksQ+rxoqmUG32L6R1IbDH43i/Z9CpwBT6mSc9D9mXs=
+github.com/mdlayher/netlink v1.8.1-0.20251028132421-dcc6cab9a6eb/go.mod h1:JVS396/XsyadqrXigRedyb1Q7yEfSq95lheQxgyQ6xA=
 github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
 github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
 github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
@@ -10,9 +14,13 @@ github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1Y
 github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
 golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
 golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
+golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
 golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
 golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
 golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
+golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
diff --git a/table.go b/table.go
index ce737a6..1304b24 100644
--- a/table.go
+++ b/table.go
@@ -15,8 +15,10 @@
 package nftables
 
 import (
+	"encoding/binary"
 	"fmt"
 
+	"github.com/google/nftables/binaryutil"
 	"github.com/mdlayher/netlink"
 	"golang.org/x/sys/unix"
 )
@@ -44,12 +46,41 @@ const (
 	TableFamilyBridge      TableFamily = unix.NFPROTO_BRIDGE
 )
 
+const (
+	// https://github.com/torvalds/linux/blob/cbd2257dc96e3e46217540fcb095a757ffa20d96/include/uapi/linux/netfilter/nf_tables.h#L211
+	nftaTableOwner = 0x07
+
+	// https://github.com/torvalds/linux/blob/cbd2257dc96e3e46217540fcb095a757ffa20d96/include/uapi/linux/netfilter/nf_tables.h#L185
+	nftTableFDormant = 0x1
+	nftTableFOwner   = 0x2
+	nftTableFPersist = 0x4
+)
+
+type TableFlags uint32
+
+const (
+	// Use TableFlagDormant to create the table in dormant state. A dormant
+	// table does not process packets until it is explicitly activated.
+	TableFlagDormant TableFlags = nftTableFDormant
+	// Use TableFlagOwner to set the owner of the table to the
+	// port ID of the creating connection. The table lifetime will
+	// then be bound to the lifetime of that connection unless the
+	// TableFlagPersist flag is also set. Should be used with lasting
+	// connections.
+	TableFlagOwner TableFlags = nftTableFOwner
+	// Use TableFlagPersist to make the table persistent, when used
+	// together with TableFlagOwner. A persistent table is not
+	// automatically removed when the creating connection is closed.
+	TableFlagPersist TableFlags = nftTableFPersist
+)
+
 // A Table contains Chains. See also
 // https://wiki.nftables.org/wiki-nftables/index.php/Configuring_tables
 type Table struct {
-	Name   string // NFTA_TABLE_NAME
-	Use    uint32 // NFTA_TABLE_USE (Number of chains in table)
-	Flags  uint32 // NFTA_TABLE_FLAGS
+	Name   string     // NFTA_TABLE_NAME
+	Use    uint32     // NFTA_TABLE_USE (Number of chains in table)
+	Flags  TableFlags // NFTA_TABLE_FLAGS
+	Owner  uint32     // NFTA_TABLE_OWNER
 	Family TableFamily
 }
 
@@ -83,10 +114,21 @@ func (cc *Conn) delTable(t *Table, hdrType netlink.HeaderType) {
 func (cc *Conn) addTable(t *Table, flag netlink.HeaderFlags) *Table {
 	cc.mu.Lock()
 	defer cc.mu.Unlock()
-	data := cc.marshalAttr([]netlink.Attribute{
+	attrs := []netlink.Attribute{
 		{Type: unix.NFTA_TABLE_NAME, Data: []byte(t.Name + "\x00")},
-		{Type: unix.NFTA_TABLE_FLAGS, Data: []byte{0, 0, 0, 0}},
-	})
+	}
+	if t.Flags&TableFlagOwner == TableFlagOwner {
+		pid, err := cc.getPortIDUnderLock()
+		if err != nil {
+			cc.setErr(fmt.Errorf("failed to get port ID: %v", err))
+		}
+
+		attrs = append(attrs, netlink.Attribute{Type: nftaTableOwner, Data: binaryutil.BigEndian.PutUint32(pid)})
+	}
+
+	attrs = append(attrs, netlink.Attribute{Type: unix.NFTA_TABLE_FLAGS, Data: binaryutil.BigEndian.PutUint32(uint32(t.Flags))})
+	data := cc.marshalAttr(attrs)
+
 	cc.messages = append(cc.messages, netlinkMessage{
 		Header: netlink.Header{
 			Type:  netlink.HeaderType((unix.NFNL_SUBSYS_NFTABLES << 8) | unix.NFT_MSG_NEWTABLE),
@@ -211,6 +253,8 @@ func tableFromMsg(msg netlink.Message) (*Table, error) {
 		return nil, err
 	}
 
+	ad.ByteOrder = binary.BigEndian
+
 	for ad.Next() {
 		switch ad.Type() {
 		case unix.NFTA_TABLE_NAME:
@@ -218,7 +262,9 @@ func tableFromMsg(msg netlink.Message) (*Table, error) {
 		case unix.NFTA_TABLE_USE:
 			t.Use = ad.Uint32()
 		case unix.NFTA_TABLE_FLAGS:
-			t.Flags = ad.Uint32()
+			t.Flags = TableFlags(ad.Uint32())
+		case nftaTableOwner:
+			t.Owner = ad.Uint32()
 		}
 	}
 



Test patch:
diff --git a/nftables_test.go b/nftables_test.go
index 742dc50..445aadb 100644
--- a/nftables_test.go
+++ b/nftables_test.go
@@ -7859,3 +7859,91 @@ func TestPreservedFirstError(t *testing.T) {
 		t.Error("expected table to not exist")
 	}
 }
+
+func TestTableOwnership(t *testing.T) {
+	tests := []struct {
+		name        string
+		flags       nftables.TableFlags
+		wantListErr error
+	}{
+		{
+			name:        "owned persistent table",
+			flags:       nftables.TableFlagOwner | nftables.TableFlagPersist,
+			wantListErr: nil,
+		},
+		{
+			name:        "owned non-persistent table",
+			flags:       nftables.TableFlagOwner,
+			wantListErr: syscall.ENOENT,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			_, newNS := nftest.OpenSystemConn(t, *enableSysTests)
+			conn, err := nftables.New(nftables.WithNetNSFd(int(newNS)), nftables.AsLasting())
+			if err != nil {
+				t.Fatalf("nftables.New() failed: %v", err)
+			}
+			defer nftest.CleanupSystemConn(t, newNS)
+			defer conn.FlushRuleset()
+
+			table := &nftables.Table{
+				Name:   "test-table",
+				Family: nftables.TableFamilyIPv4,
+				Flags:  tt.flags,
+			}
+			conn.AddTable(table)
+			if err := conn.Flush(); err != nil {
+				t.Fatalf("conn.Flush() failed: %v", err)
+			}
+			gotTable, err := conn.ListTable("test-table")
+			if err != nil {
+				t.Fatalf("conn.ListTable() failed: %v", err)
+			}
+
+			portId, err := conn.GetPortID()
+			if err != nil {
+				t.Fatalf("conn.GetPortID() failed: %v", err)
+			}
+
+			if gotTable.Owner != portId {
+				t.Fatalf("expected table owner to be %d, got %d", portId, gotTable.Owner)
+			}
+
+			if err := conn.CloseLasting(); err != nil {
+				t.Fatalf("conn.CloseLasting() failed: %v", err)
+			}
+
+			newConn, err := nftables.New(nftables.WithNetNSFd(int(newNS)))
+			if err != nil {
+				t.Fatalf("nftables.New() failed: %v", err)
+			}
+			defer newConn.FlushRuleset()
+
+			gotTable, err = newConn.ListTable("test-table")
+			if !errors.Is(err, tt.wantListErr) {
+				t.Fatalf("expected ListTable error to be %v, got %v", tt.wantListErr, err)
+			}
+
+			if tt.wantListErr == nil && gotTable.Owner != 0 {
+				t.Fatalf("expected table owner to be reset after closing connection, got %d", gotTable.Owner)
+			}
+		})
+	}
+}
+
+func TestGetPortID(t *testing.T) {
+	conn, newNS := nftest.OpenSystemConn(t, *enableSysTests)
+	defer nftest.CleanupSystemConn(t, newNS)
+	defer conn.FlushRuleset()
+
+	pid, err := conn.GetPortID()
+	if err != nil {
+		t.Fatalf("conn.GetPortID() failed: %v", err)
+	}
+
+	if pid == 0 {
+		t.Fatalf("conn.GetPortID() returned invalid port ID: %d", pid)
+	}
+}