google/nftables

invalid argument for match

joewilliams opened this issue ยท 10 comments

I am attempting to use a match expression and running into an invalid argument error I do not understand:

	matchTable := c.AddTable(&nftables.Table{
		Family: nftables.TableFamilyIPv4,
		Name:   "match",
	})

	matchChain := c.AddChain(&nftables.Chain{
		Name:  "match-filter",
		Table: matchTable,
		Type:  nftables.ChainTypeFilter,
	})

	c.AddRule(&nftables.Rule{
		Table: matchTable,
		Chain: matchChain,
		Exprs: []expr.Any{
			&expr.Match{
				Name: "addrtype",
				Rev:  1,
				Info: &xt.AddrType{
					Source: unix.RTN_LOCAL,
				},
			},
		},
	})

	if err := c.Flush(); err != nil {
		log.Fatalf("nftables.Flush() failed: %v", err)
	}

The above code results in

2022/09/23 02:21:07 nftables.Flush() failed: conn.Receive: netlink receive: invalid argument

I am pretty sure this is equivalent to something like iptables -A match-filter -m addrtype --src-type LOCAL which does work.

cc @thediveo since I think they worked on this most recently.

Thanks!

I think I may have found the problem with this. Attempting to do the equivalent with raw netlink messages I was able to reproduce the same invalid argument error. It seems to stem from the attributes when creating rules.

{Type: unix.NLA_F_NESTED | unix.NFTA_RULE_EXPRESSIONS, Data: cc.marshalAttr(exprAttrs)},

https://github.com/google/nftables/blob/main/rule.go#L133

In the case of most rules the above works fine but in the case of a match I believe unix.NLA_F_NESTED needs to be unix.NFTA_MATCH_NAME. In my test code switching to unix.NFTA_MATCH_NAME the invalid argument error goes away.

I would generally recommend using nft when exploring nftables, instead of using this package. That should give you better error messages and input validation, and more recipes to look at online. Afterwards, you can translate what nft does into Go.

Iโ€™ll close this issue since it sounds like you figured out the issue.

@stapelberg unless I am mistaken adding a match expression rule is broken due to the above NLA_F_NESTED vs NFTA_MATCH_NAME problem in this library itself.

Hi @joewilliams,

I am not that familiar with match rules so I might be wrong, but I would suggest trying another approach before making any changes to the current match implementation.

I have successfully replicated and resolved your issue with the following code:

# nft flush ruleset
# cat main.go
package main

import (
	"fmt"

	"github.com/google/nftables/binaryutil"
	"github.com/google/nftables/expr"
	"github.com/google/nftables"
	"golang.org/x/sys/unix"
)

func main() {
	c, _ := nftables.New()

	matchTable := c.AddTable(&nftables.Table{
		Family: nftables.TableFamilyIPv4,
		Name:   "filter",
	})

	matchChain := c.AddChain(&nftables.Chain{
		Name:  "forward",
		Table: matchTable,
		Type:  nftables.ChainTypeFilter,
	})

	c.AddRule(&nftables.Rule{
		Table: matchTable,
		Chain: matchChain,
		Exprs: []expr.Any{
			&expr.Fib{
				Register: 1,
				ResultADDRTYPE: true,
				FlagSADDR: true,
			},
			&expr.Cmp{
				Op: expr.CmpOpEq,
				Register: 1,
				Data: binaryutil.NativeEndian.PutUint32(unix.RTN_LOCAL),
			},
		},
	})

	if err := c.Flush(); err != nil {
		fmt.Printf("nftables.Flush() failed: %v", err)
	}
}
# go run main.go
# nft list ruleset
table ip filter {
	chain forward {
		fib saddr type local
	}
}

Here are some translation examples that I have found which might help you in constructing other nftables rule expressions: https://git.netfilter.org/iptables/tree/extensions/libxt_addrtype.txlate?id=d1aa01483b5cac8c70c9385033e60efd7a744e1f.

I hope this resolves your issue.

Thanks @turekt, it's very nice of you to work up an example! Unfortunately I need to specifically use match rules for my use case.

I have also tried a UDP based match and get the same invalid argument error.

				&expr.Match{
					Name: "udp",
					Rev:  0,
					Info: &xt.Udp{
						DstPorts: [2]uint16{uint16(50), uint16(60)},
					},
				},
				&expr.Verdict{
					Kind: expr.VerdictDrop,
				},
2022/09/30 16:11:49 nftables.Flush() failed: conn.Receive: netlink receive: invalid argument

This cooresponds with:

Sep 30 16:11:49 dev kernel: [552795.419956] x_tables: ip_tables: udp match: only valid for protocol 17

Using my own fork changing NLA_F_NESTED to NFTA_MATCH_NAME on https://github.com/google/nftables/blob/main/rule.go#L133 fixes the error but unfortunately the rule seems "blank". Maybe because nft doesn't know how to print it?

The kernel log error message goes away as well with the above change.

Even though the error goes away the traffic is not blocked as a result of the above rule. I would expect outgoing DNS traffic to stop.

What GetRules gets back:

rule 0: &nftables.Rule{Table:(*nftables.Table)(0x40000b2000), Chain:(*nftables.Chain)(0x400009a140), Position:0x0, Handle:0x2, Flags:0x0, Exprs:[]expr.Any{}, UserData:[]uint8(nil)}
$ sudo nft --debug all list ruleset
Entering state 0
Reducing stack by rule 1 (line 808):
-> $$ = nterm input (: )
Stack now 0
Entering state 1
Reading a token: --accepting rule at line 292 ("list")
Next token is token "list" (: )
Shifting token "list" (: )
Entering state 26
Reading a token: --accepting rule at line 653 (" ")
--accepting rule at line 266 ("ruleset")
Next token is token "ruleset" (: )
Shifting token "ruleset" (: )
Entering state 114
Reading a token: --accepting rule at line 647 ("
")
Next token is token "newline" (: )
Reducing stack by rule 331 (line 2350):
-> $$ = nterm ruleset_spec (: )
Stack now 0 1 26 114
Entering state 444
Reducing stack by rule 114 (line 1300):
   $1 = token "ruleset" (: )
   $2 = nterm ruleset_spec (: )
-> $$ = nterm list_cmd (: )
Stack now 0 1 26
Entering state 129
Reducing stack by rule 23 (line 904):
   $1 = token "list" (: )
   $2 = nterm list_cmd (: )
-> $$ = nterm base_cmd (: )
Stack now 0 1
Entering state 46
Next token is token "newline" (: )
Shifting token "newline" (: )
Entering state 4
Reducing stack by rule 3 (line 818):
   $1 = token "newline" (: )
-> $$ = nterm stmt_separator (: )
Stack now 0 1 46
Entering state 275
Reducing stack by rule 14 (line 876):
   $1 = nterm base_cmd (: )
   $2 = nterm stmt_separator (: )
-> $$ = nterm line (: )
Stack now 0 1
Entering state 45
Reducing stack by rule 2 (line 809):
   $1 = nterm input (: )
   $2 = nterm line (: )
-> $$ = nterm input (: )
Stack now 0
Entering state 1
Reading a token: --(end of buffer or a NUL)
--EOF (start condition 0)
Now at end of input.
Shifting token "end of file" (: )
Entering state 2
Stack now 0 1 2
Cleanup: popping token "end of file" (: )
Cleanup: popping nterm input (: )
----------------	------------------
|  0000000020  |	| message length |
| 02576 | R--- |	|  type | flags  |
|  0000000000  |	| sequence number|
|  0000000000  |	|     port ID    |
----------------	------------------
| 00 00 00 00  |	|  extra header  |
----------------	------------------
----------------	------------------
|  0000000020  |	| message length |
| 02561 | R--- |	|  type | flags  |
|  0000000000  |	| sequence number|
|  0000000000  |	|     port ID    |
----------------	------------------
| 00 00 00 00  |	|  extra header  |
----------------	------------------
----------------	------------------
|  0000000036  |	| message length |
| 02570 | R--- |	|  type | flags  |
|  0000000000  |	| sequence number|
|  0000000000  |	|     port ID    |
----------------	------------------
| 02 00 00 00  |	|  extra header  |
|00015|--|00001|	|len |flags| type|
| 6d 61 74 63  |	|      data      |	 m a t c
| 68 74 61 62  |	|      data      |	 h t a b
| 6c 65 00 00  |	|      data      |	 l e
----------------	------------------
----------------	------------------
|  0000000020  |	| message length |
| 02564 | R--- |	|  type | flags  |
|  0000000000  |	| sequence number|
|  0000000000  |	|     port ID    |
----------------	------------------
| 02 00 00 00  |	|  extra header  |
----------------	------------------
----------------	------------------
|  0000000036  |	| message length |
| 02583 | R--- |	|  type | flags  |
|  0000000000  |	| sequence number|
|  0000000000  |	|     port ID    |
----------------	------------------
| 02 00 00 00  |	|  extra header  |
|00015|--|00001|	|len |flags| type|
| 6d 61 74 63  |	|      data      |	 m a t c
| 68 74 61 62  |	|      data      |	 h t a b
| 6c 65 00 e0  |	|      data      |	 l e
----------------	------------------
----------------	------------------
|  0000000036  |	| message length |
| 02579 | R--- |	|  type | flags  |
|  0000000000  |	| sequence number|
|  0000000000  |	|     port ID    |
----------------	------------------
| 02 00 00 00  |	|  extra header  |
|00015|--|00001|	|len |flags| type|
| 6d 61 74 63  |	|      data      |	 m a t c
| 68 74 61 62  |	|      data      |	 h t a b
| 6c 65 00 00  |	|      data      |	 l e
----------------	------------------
----------------	------------------
|  0000000020  |	| message length |
| 02567 | R--- |	|  type | flags  |
|  0000000000  |	| sequence number|
|  0000000000  |	|     port ID    |
----------------	------------------
| 02 00 00 00  |	|  extra header  |
----------------	------------------
ip matchtable output 2

update network layer protocol context:
 link layer          : none
 network layer       : ip <-
 transport layer     : none

----------------	------------------
|  0000000020  |	| message length |
| 02576 | R--- |	|  type | flags  |
|  0000000000  |	| sequence number|
|  0000000000  |	|     port ID    |
----------------	------------------
| 00 00 00 00  |	|  extra header  |
----------------	------------------
Evaluate list
list ruleset
^^^^^^^^^^^^^


table ip matchtable {
	chain output {
		type filter hook output priority -2147483648; policy accept;

	}
}

Trying this from a different angle, I used iptables-nft to create a conntrack match which is applied as a match in nftables.

sudo iptables-nft -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

This seems to come back correctly:

rule 0: &nftables.Rule{Table:(*nftables.Table)(0x40000b4000), Chain:(*nftables.Chain)(0x400009a100), Position:0x0, Handle:0x4, Flags:0x0, Exprs:[]expr.Any{(*expr.Match)(0x4000086930), (*expr.Counter)(0x40000a0420), (*expr.Verdict)(0x400008e168)}, UserData:[]uint8(nil)}
--- expr: 0: &expr.Match{Name:"conntrack", Rev:0x3, Info:(*xt.ConntrackMtinfo3)(0x40000921e0)}
--- expr: 1: &expr.Counter{Bytes:0x0, Packets:0x0}
--- expr: 2: &expr.Verdict{Kind:1, Chain:""}

So my guess is that some match types are encoded/decoded correctly but others are not.

I would expect the UDP match above to come back from GetRules the same as this conntrack one.

I am not sure what's up with the UDP match support but it seems like my original problem was using Rev:1 and xt.AddrType rather than xt.AddrType1.

The xt parts are extremely nft version dependent. The nftables project only regards the nft tool as the official tool, but not the netlink messages. Personally, I find this a bad decision, as it forces devs to parse their special format that doesn't seem to be properly documented. Alas, unfortunately the xt territory is something where nftables do as they like and that's probably the reason for several different variants of basically the same data structures.

@thediveo thanks for the info!