From 910315ba22a8fc2f38e3d0e2ac84c670deb2ec82 Mon Sep 17 00:00:00 2001 From: wiegratz Date: Mon, 18 Mar 2024 13:35:32 +0000 Subject: [PATCH 1/3] test: add test for synproxy --- resources/test/json/synproxy.json | 299 ++++++++++++++++++++++++++++++ resources/test/nft/synproxy.nft | 41 ++++ 2 files changed, 340 insertions(+) create mode 100644 resources/test/json/synproxy.json create mode 100644 resources/test/nft/synproxy.nft diff --git a/resources/test/json/synproxy.json b/resources/test/json/synproxy.json new file mode 100644 index 0000000..86a6640 --- /dev/null +++ b/resources/test/json/synproxy.json @@ -0,0 +1,299 @@ +{ + "nftables": [ + { + "metainfo": { + "version": "1.0.6", + "release_name": "Lester Gooch #5", + "json_schema_version": 1 + } + }, + { + "table": { + "family": "ip", + "name": "synproxy_anonymous", + "handle": 1 + } + }, + { + "chain": { + "family": "ip", + "table": "synproxy_anonymous", + "name": "PREROUTING", + "handle": 1, + "type": "filter", + "hook": "prerouting", + "prio": -300, + "policy": "accept" + } + }, + { + "chain": { + "family": "ip", + "table": "synproxy_anonymous", + "name": "INPUT", + "handle": 2, + "type": "filter", + "hook": "input", + "prio": 0, + "policy": "accept" + } + }, + { + "rule": { + "family": "ip", + "table": "synproxy_anonymous", + "chain": "PREROUTING", + "handle": 3, + "expr": [ + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "tcp", + "field": "dport" + } + }, + "right": 8080 + } + }, + { + "match": { + "op": "in", + "left": { + "payload": { + "protocol": "tcp", + "field": "flags" + } + }, + "right": "syn" + } + }, + { + "notrack": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "synproxy_anonymous", + "chain": "INPUT", + "handle": 4, + "expr": [ + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "tcp", + "field": "dport" + } + }, + "right": 8080 + } + }, + { + "match": { + "op": "in", + "left": { + "ct": { + "key": "state" + } + }, + "right": [ + "invalid", + "untracked" + ] + } + }, + { + "synproxy": { + "mss": 1460, + "wscale": 7, + "flags": [ + "timestamp", + "sack-perm" + ] + } + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "synproxy_anonymous", + "chain": "INPUT", + "handle": 5, + "expr": [ + { + "match": { + "op": "in", + "left": { + "ct": { + "key": "state" + } + }, + "right": "invalid" + } + }, + { + "drop": null + } + ] + } + }, + { + "table": { + "family": "ip", + "name": "synproxy_named", + "handle": 2 + } + }, + { + "synproxy": { + "family": "ip", + "name": "synproxy_named_1", + "table": "synproxy_named", + "handle": 3, + "mss": 1460, + "wscale": 7, + "flags": [ + "timestamp", + "sack-perm" + ] + } + }, + { + "synproxy": { + "family": "ip", + "name": "synproxy_named_2", + "table": "synproxy_named", + "handle": 4, + "mss": 1460, + "wscale": 5 + } + }, + { + "chain": { + "family": "ip", + "table": "synproxy_named", + "name": "PREROUTING", + "handle": 1, + "type": "filter", + "hook": "prerouting", + "prio": -300, + "policy": "accept" + } + }, + { + "chain": { + "family": "ip", + "table": "synproxy_named", + "name": "FORWARD", + "handle": 2, + "type": "filter", + "hook": "forward", + "prio": 0, + "policy": "accept" + } + }, + { + "rule": { + "family": "ip", + "table": "synproxy_named", + "chain": "PREROUTING", + "handle": 5, + "expr": [ + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "tcp", + "field": "dport" + } + }, + "right": 8080 + } + }, + { + "match": { + "op": "in", + "left": { + "payload": { + "protocol": "tcp", + "field": "flags" + } + }, + "right": "syn" + } + }, + { + "notrack": null + } + ] + } + }, + { + "rule": { + "family": "ip", + "table": "synproxy_named", + "chain": "FORWARD", + "handle": 7, + "expr": [ + { + "match": { + "op": "in", + "left": { + "ct": { + "key": "state" + } + }, + "right": [ + "invalid", + "untracked" + ] + } + }, + { + "synproxy": { + "map": { + "key": { + "payload": { + "protocol": "ip", + "field": "saddr" + } + }, + "data": { + "set": [ + [ + { + "prefix": { + "addr": "192.168.1.0", + "len": 24 + } + }, + "synproxy_named_1" + ], + [ + { + "prefix": { + "addr": "192.168.2.0", + "len": 24 + } + }, + "synproxy_named_2" + ] + ] + } + } + } + } + ] + } + } + ] +} diff --git a/resources/test/nft/synproxy.nft b/resources/test/nft/synproxy.nft new file mode 100644 index 0000000..f88a4ee --- /dev/null +++ b/resources/test/nft/synproxy.nft @@ -0,0 +1,41 @@ +table ip synproxy_anonymous { + + chain PREROUTING { + type filter hook prerouting priority raw; policy accept; + tcp dport 8080 tcp flags syn notrack + } + + chain INPUT { + type filter hook input priority filter; policy accept; + tcp dport 8080 ct state invalid,untracked synproxy mss 1460 wscale 7 timestamp sack-perm + ct state invalid drop + } +} + +table ip synproxy_named { + + synproxy synproxy_named_1 { + mss 1460 + wscale 7 + timestamp sack-perm + } + + synproxy synproxy_named_2 { + mss 1460 + wscale 5 + } + + chain PREROUTING { + type filter hook prerouting priority raw; policy accept; + tcp dport 8080 tcp flags syn notrack + } + + chain FORWARD { + type filter hook forward priority filter; policy accept; + + ct state invalid,untracked synproxy name ip saddr map { + 192.168.1.0/24 : "synproxy_named_1", + 192.168.2.0/24 : "synproxy_named_2", + } + } +} From 3799022069311f47770aa061da5c05bf70e306bb Mon Sep 17 00:00:00 2001 From: wiegratz Date: Mon, 18 Mar 2024 13:40:21 +0000 Subject: [PATCH 2/3] test: refactor nftables-json test script with unshare --- resources/test/nft-to-json.sh | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/resources/test/nft-to-json.sh b/resources/test/nft-to-json.sh index 7ae9b8f..31f2969 100755 --- a/resources/test/nft-to-json.sh +++ b/resources/test/nft-to-json.sh @@ -8,14 +8,7 @@ OUTPUT_DIR=./json convert_file () { INFILE=$1 - NETNS=nftables - ip netns delete $NETNS 2>/dev/null || true - ip netns add $NETNS - ( - ip netns exec $NETNS nft -f "${INFILE}" - ip netns exec $NETNS nft -j list ruleset - ) || true - ip netns delete $NETNS + unshare -rn sh -exc "nft -f \"${INFILE}\" && nft -j list ruleset" } for nftfile in "$INPUT_DIR"/*.nft; do From 0108fbfc9ecf6523083b4bd77215431a90e11c16 Mon Sep 17 00:00:00 2001 From: wiegratz Date: Mon, 18 Mar 2024 15:00:45 +0000 Subject: [PATCH 3/3] feat: add synproxy statement and list object also mark some other statements that are missing. compare with nftables json_parse_stmt: https://git.netfilter.org/nftables/tree/src/parser_json.c?id=c4c740117f6fbf39dd67dd87635ea8b497718ad7#n2884 --- src/schema.rs | 29 +++++++++++++++++++++++++++++ src/stmt.rs | 26 ++++++++++++++++++++++++-- src/types.rs | 11 +++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/schema.rs b/src/schema.rs index 212e5e3..0beed2e 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -42,6 +42,7 @@ pub enum NfListObject { CTTimeout(CTTimeout), #[serde(rename = "ct expectation")] CTExpectation(CTExpectation), + SynProxy(SynProxy), } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] @@ -449,3 +450,31 @@ pub struct CTExpectation { #[serde(skip_serializing_if = "Option::is_none")] pub size: Option, } + +/// [SynProxy] intercepts new TCP connections and handles the initial 3-way handshake using +/// syncookies instead of conntrack to establish the connection. +/// +/// Named SynProxy requires **nftables 0.9.3 or newer**. +/// +/// [SynProxy]: https://wiki.nftables.org/wiki-nftables/index.php/Synproxy +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct SynProxy { + /// The table’s family. + pub family: NfFamily, + /// The table’s name. + pub table: String, + /// The synproxy's name. + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + /// The synproxy's handle. For input, it is used by the [delete command][NfCmd::Delete] only. + pub handle: Option, + #[serde(skip_serializing_if = "Option::is_none")] + /// The maximum segment size (must match your backend server). + pub mss: Option, + #[serde(skip_serializing_if = "Option::is_none")] + /// The window scale (must match your backend server). + pub wscale: Option, + #[serde(skip_serializing_if = "Option::is_none")] + /// The synproxy's [flags][crate::types::SynProxyFlag]. + pub flags: Option>, +} diff --git a/src/stmt.rs b/src/stmt.rs index 5845c56..c0d8f63 100644 --- a/src/stmt.rs +++ b/src/stmt.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use strum_macros::EnumString; -use crate::types::RejectCode; +use crate::types::{RejectCode, SynProxyFlag}; use crate::visitor::single_string_to_option_hashset_logflag; use crate::expr::Expression; @@ -36,6 +36,7 @@ pub enum Statement { #[serde(rename = "quota")] /// reference to a named quota object QuotaRef(String), + // TODO: last Limit(Limit), /// The Flow statement offloads matching network traffic to flowtables, @@ -51,6 +52,7 @@ pub enum Statement { Redirect(Option), // redirect is subset of NAT options Reject(Option), Set(Set), + // TODO: map Log(Option), #[serde(rename = "ct helper")] @@ -60,6 +62,7 @@ pub enum Statement { Meter(Meter), Queue(Queue), #[serde(rename = "vmap")] + // TODO: vmap is expr, not stmt! VerdictMap(VerdictMap), #[serde(rename = "ct count")] @@ -76,9 +79,12 @@ pub enum Statement { /// This represents an xt statement from xtables compat interface. /// Sadly, at this point, it is not possible to provide any further information about its content. XT(Option), - + /// A netfilter synproxy intercepts new TCP connections and handles the initial 3-way handshake using syncookies instead of conntrack to establish the connection. + SynProxy(SynProxy), /// Redirects the packet to a local socket without changing the packet header in any way. TProxy(TProxy), + // TODO: reset + // TODO: secmark } #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] @@ -444,6 +450,22 @@ pub struct CTCount { pub inv: Option, } +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +/// Limit the number of connections using conntrack. +/// +/// Anonymous synproxy was requires **nftables 0.9.2 or newer**. +pub struct SynProxy { + #[serde(skip_serializing_if = "Option::is_none")] + /// maximum segment size (must match your backend server) + pub mss: Option, + #[serde(skip_serializing_if = "Option::is_none")] + /// window scale (must match your backend server) + pub wscale: Option, + #[serde(skip_serializing_if = "Option::is_none")] + /// The synproxy's [flags][crate::types::SynProxyFlag]. + pub flags: Option>, +} + #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] /// Redirects the packet to a local socket without changing the packet header in any way. diff --git a/src/types.rs b/src/types.rs index 74a08ad..1b75b05 100644 --- a/src/types.rs +++ b/src/types.rs @@ -90,3 +90,14 @@ pub enum RejectCode { /// Address unreachable (ICMPv6) AddrUnreach, } + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)] +#[serde(rename_all = "lowercase")] +/// Describes a SynProxy's flags. +pub enum SynProxyFlag { + /// Pass client timestamp option to backend. + Timestamp, + #[serde(rename = "sack-perm")] + /// Pass client selective acknowledgement option to backend. + SackPerm, +}