diff --git a/doc/ref/ruleset-1.json b/doc/ref/ruleset-1.json new file mode 100644 index 0000000..68752c0 --- /dev/null +++ b/doc/ref/ruleset-1.json @@ -0,0 +1 @@ +{"nftables": [{"metainfo": {"version": "1.1.6", "release_name": "Commodore Bullmoose #7", "json_schema_version": 1}}, {"table": {"family": "inet", "name": "fwl", "handle": 23}}, {"chain": {"family": "inet", "table": "fwl", "name": "wg_flow", "handle": 1}}, {"chain": {"family": "inet", "table": "fwl", "name": "blockOutboundWG", "handle": 2}}, {"chain": {"family": "inet", "table": "fwl", "name": "input", "handle": 3, "type": "filter", "hook": "input", "prio": 0, "policy": "drop"}}, {"chain": {"family": "inet", "table": "fwl", "name": "forward", "handle": 4, "type": "filter", "hook": "forward", "prio": 0, "policy": "drop"}}, {"chain": {"family": "inet", "table": "fwl", "name": "output", "handle": 5, "type": "filter", "hook": "output", "prio": 0, "policy": "accept"}}, {"chain": {"family": "inet", "table": "fwl", "name": "nat_prerouting", "handle": 6, "type": "nat", "hook": "prerouting", "prio": -100, "policy": "accept"}}, {"chain": {"family": "inet", "table": "fwl", "name": "nat_postrouting", "handle": 7, "type": "nat", "hook": "postrouting", "prio": 100, "policy": "accept"}}, {"set": {"family": "inet", "name": "rfc1918", "table": "fwl", "type": "ipv4_addr", "handle": 8, "flags": ["interval"], "elem": [{"prefix": {"addr": "10.0.0.0", "len": 8}}, {"prefix": {"addr": "172.16.0.0", "len": 12}}, {"prefix": {"addr": "192.168.0.0", "len": 16}}]}}, {"map": {"family": "inet", "name": "forwards", "table": "fwl", "type": ["inet_proto", "inet_service"], "handle": 9, "map": ["ipv4_addr", "inet_service"], "elem": [[{"concat": ["tcp", 8080]}, {"concat": ["10.17.1.10", 80]}], [{"concat": ["tcp", 2222]}, {"concat": ["10.17.1.11", 22]}]]}}, {"rule": {"family": "inet", "table": "fwl", "chain": "wg_flow", "handle": 10, "expr": [{"match": {"op": "in", "left": {"ct": {"key": "state"}}, "right": "new"}}, {"match": {"op": "==", "left": {"ct": {"key": "mark"}}, "right": 0}}, {"match": {"op": "==", "left": {"payload": {"protocol": "udp", "field": "length"}}, "right": 156}}, {"match": {"op": "==", "left": {"payload": {"base": "th", "offset": 64, "len": 8}}, "right": 1}}, {"mangle": {"key": {"ct": {"key": "mark"}}, "value": 1}}, {"return": null}]}}, {"rule": {"family": "inet", "table": "fwl", "chain": "wg_flow", "handle": 11, "expr": [{"match": {"op": "==", "left": {"ct": {"key": "mark"}}, "right": 1}}, {"match": {"op": "==", "left": {"payload": {"protocol": "udp", "field": "length"}}, "right": 100}}, {"match": {"op": "==", "left": {"payload": {"base": "th", "offset": 64, "len": 8}}, "right": 2}}, {"mangle": {"key": {"ct": {"key": "mark"}}, "value": 2}}, {"return": null}]}}, {"rule": {"family": "inet", "table": "fwl", "chain": "blockOutboundWG", "handle": 12, "expr": [{"match": {"op": "==", "left": {"meta": {"key": "nfproto"}}, "right": "ipv4"}}, {"match": {"op": "==", "left": {"payload": {"protocol": "udp", "field": "length"}}, "right": 156}}, {"match": {"op": "==", "left": {"payload": {"base": "th", "offset": 64, "len": 8}}, "right": 1}}, {"jump": {"target": "wg_flow"}}]}}, {"rule": {"family": "inet", "table": "fwl", "chain": "blockOutboundWG", "handle": 13, "expr": [{"match": {"op": "==", "left": {"ct": {"key": "mark"}}, "right": 2}}, {"log": {"prefix": "WG blocked: "}}, {"drop": null}]}}, {"rule": {"family": "inet", "table": "fwl", "chain": "blockOutboundWG", "handle": 14, "expr": [{"return": null}]}}, {"rule": {"family": "inet", "table": "fwl", "chain": "input", "handle": 16, "expr": [{"match": {"op": "==", "left": {"ct": {"key": "state"}}, "right": {"set": ["established", "related"]}}}, {"accept": null}]}}, {"rule": {"family": "inet", "table": "fwl", "chain": "input", "handle": 17, "expr": [{"match": {"op": "==", "left": {"meta": {"key": "iifname"}}, "right": "lo"}}, {"accept": null}]}}, {"rule": {"family": "inet", "table": "fwl", "chain": "input", "handle": 18, "expr": [{"match": {"op": "==", "left": {"payload": {"protocol": "ip6", "field": "nexthdr"}}, "right": "ipv6-icmp"}}, {"match": {"op": "==", "left": {"payload": {"protocol": "ip6", "field": "saddr"}}, "right": {"prefix": {"addr": "fe80::", "len": 10}}}}, {"accept": null}]}}, {"rule": {"family": "inet", "table": "fwl", "chain": "input", "handle": 19, "expr": [{"match": {"op": "==", "left": {"meta": {"key": "nfproto"}}, "right": "ipv4"}}, {"match": {"op": "==", "left": {"payload": {"protocol": "tcp", "field": "dport"}}, "right": 22}}, {"accept": null}]}}, {"rule": {"family": "inet", "table": "fwl", "chain": "input", "handle": 20, "expr": [{"match": {"op": "==", "left": {"meta": {"key": "nfproto"}}, "right": "ipv4"}}, {"match": {"op": "==", "left": {"payload": {"protocol": "udp", "field": "dport"}}, "right": 51944}}, {"accept": null}]}}, {"rule": {"family": "inet", "table": "fwl", "chain": "forward", "handle": 22, "expr": [{"match": {"op": "==", "left": {"ct": {"key": "state"}}, "right": {"set": ["established", "related"]}}}, {"accept": null}]}}, {"rule": {"family": "inet", "table": "fwl", "chain": "forward", "handle": 24, "expr": [{"match": {"op": "==", "left": {"meta": {"key": "iifname"}}, "right": {"set": ["wg0", "lan"]}}}, {"match": {"op": "==", "left": {"meta": {"key": "oifname"}}, "right": "wan"}}, {"jump": {"target": "blockOutboundWG"}}]}}, {"rule": {"family": "inet", "table": "fwl", "chain": "forward", "handle": 25, "expr": [{"match": {"op": "in", "left": {"ct": {"key": "status"}}, "right": "dnat"}}, {"accept": null}]}}, {"rule": {"family": "inet", "table": "fwl", "chain": "forward", "handle": 27, "expr": [{"match": {"op": "==", "left": {"meta": {"key": "iifname"}}, "right": {"set": ["wg0", "lan"]}}}, {"match": {"op": "==", "left": {"meta": {"key": "oifname"}}, "right": "wan"}}, {"accept": null}]}}, {"rule": {"family": "inet", "table": "fwl", "chain": "forward", "handle": 30, "expr": [{"match": {"op": "==", "left": {"meta": {"key": "iifname"}}, "right": {"set": ["wg0", "lan"]}}}, {"match": {"op": "==", "left": {"meta": {"key": "oifname"}}, "right": {"set": ["wg0", "lan"]}}}, {"accept": null}]}}, {"rule": {"family": "inet", "table": "fwl", "chain": "forward", "handle": 33, "expr": [{"match": {"op": "==", "left": {"meta": {"key": "iifname"}}, "right": "wan"}}, {"match": {"op": "==", "left": {"meta": {"key": "oifname"}}, "right": {"set": ["wg0", "lan"]}}}, {"match": {"op": "==", "left": {"meta": {"key": "nfproto"}}, "right": "ipv4"}}, {"match": {"op": "==", "left": {"meta": {"key": "l4proto"}}, "right": {"set": ["tcp", "udp"]}}}, {"match": {"op": "==", "left": {"concat": [{"meta": {"key": "l4proto"}}, {"payload": {"protocol": "th", "field": "dport"}}]}, "right": "@forwards"}}, {"accept": null}]}}, {"rule": {"family": "inet", "table": "fwl", "chain": "nat_prerouting", "handle": 35, "expr": [{"match": {"op": "==", "left": {"meta": {"key": "nfproto"}}, "right": "ipv4"}}, {"match": {"op": "==", "left": {"meta": {"key": "l4proto"}}, "right": {"set": ["tcp", "udp"]}}}, {"match": {"op": "==", "left": {"fib": {"result": "type", "flags": ["daddr"]}}, "right": "local"}}, {"dnat": {"family": "ip", "addr": {"map": {"key": {"concat": [{"meta": {"key": "l4proto"}}, {"payload": {"protocol": "th", "field": "dport"}}]}, "data": "@forwards"}}}}]}}, {"rule": {"family": "inet", "table": "fwl", "chain": "nat_postrouting", "handle": 36, "expr": [{"match": {"op": "==", "left": {"meta": {"key": "oifname"}}, "right": "wan"}}, {"match": {"op": "==", "left": {"payload": {"protocol": "ip", "field": "saddr"}}, "right": "@rfc1918"}}, {"masquerade": null}]}}]} diff --git a/doc/ref/ruleset-1.nft b/doc/ref/ruleset-1.nft new file mode 100644 index 0000000..319a258 --- /dev/null +++ b/doc/ref/ruleset-1.nft @@ -0,0 +1,163 @@ +#!/usr/sbin/nft -f +# Compiled from examples/router.fwl +# Single inet table: fwl + +flush ruleset + +table inet fwl { + + # ── Data: let rfc1918 ──────────────────────────────────────────────────── + set rfc1918 { + type ipv4_addr + flags interval + elements = { + 10.0.0.0/8, + 172.16.0.0/12, + 192.168.0.0/16 + } + } + + # ── Data: let forwards ────────────────────────────────────────────────── + map forwards { + type inet_proto . inet_service : ipv4_addr . inet_service + elements = { + tcp . 8080 : 10.17.1.10 . 80, + tcp . 2222 : 10.17.1.11 . 22 + } + } + + # ── WireGuard ct mark state machine ───────────────────────────────────── + # Compiles: flow WireGuardHandshake = WGInitiation . WGResponse within 5s + # State: ct mark 0 = Idle, 1 = SawInitiation, 2 = Confirmed + # + # WGInitiation: UDP, udp length == 156 (8 hdr + 148 payload), payload[0] == 0x01 + # WGResponse: UDP, udp length == 100 (8 hdr + 92 payload), payload[0] == 0x02 + # @th,64,8 = first byte of UDP payload (offset 64 bits past transport header start) + + chain wg_flow { + # Packet 1: Idle → SawInitiation + ct state new ct mark 0 \ + meta l4proto udp udp length 156 \ + @th,64,8 0x01 \ + ct mark set 1 \ + return + + # Packet 2: SawInitiation → Confirmed + ct mark 1 \ + meta l4proto udp udp length 100 \ + @th,64,8 0x02 \ + ct mark set 2 \ + return + } + + # ── rule blockOutboundWG ───────────────────────────────────────────────── + # Compiles: rule blockOutboundWG : Frame -> Action + # Called via jump from forward. Drops confirmed WG handshakes, returns otherwise. + + chain blockOutboundWG { + # Feed matching UDP into the WG state machine + meta nfproto ipv4 meta l4proto udp \ + udp length 156 \ + @th,64,8 0x01 \ + jump wg_flow + + # If handshake is now Confirmed (ct mark 2): log + drop + ct mark 2 \ + log prefix "WG blocked: " level warn \ + drop + + # Continue: return to forward chain (no verdict) + return + } + + # ── policy input ───────────────────────────────────────────────────────── + # hook = Input, table = Filter, priority = filter (0), default = drop + + chain input { + type filter hook input priority filter; policy drop; + + # | _ if ct.state in { Established, Related } -> Allow + ct state { established, related } accept + + # | Frame(lo, _) -> Allow + iifname "lo" accept + + # | Frame(_, IPv6(ip6, ICMPv6(_, _))) if ip6.src in fe80::/10 -> Allow + meta nfproto ipv6 ip6 nexthdr ipv6-icmp ip6 saddr fe80::/10 accept + + # | Frame(_, IPv4(_, TCP(tcp, _))) if tcp.dport == :22 -> Allow + meta nfproto ipv4 meta l4proto tcp tcp dport 22 accept + + # | Frame(_, IPv4(_, UDP(udp, _))) if udp.dport == :51944 -> Allow + meta nfproto ipv4 meta l4proto udp udp dport 51944 accept + + # | _ -> Drop (chain policy) + } + + # ── policy forward ─────────────────────────────────────────────────────── + # hook = Forward, table = Filter, priority = filter (0), default = drop + + chain forward { + type filter hook forward priority filter; policy drop; + + # | _ if ct.state in { Established, Related } -> Allow + ct state { established, related } accept + + # | frame if iif in lan_zone && oif == wan -> blockOutboundWG(frame) + meta iifname { "lan", "wg0" } meta oifname "wan" jump blockOutboundWG + + # | _ if ct.status == DNAT -> Allow + ct status dnat accept + + # | Frame(iif in lan_zone -> wan, _) -> Allow + meta iifname { "lan", "wg0" } meta oifname "wan" accept + + # | Frame(iif in lan_zone -> lan_zone, _) -> Allow + meta iifname { "lan", "wg0" } meta oifname { "lan", "wg0" } accept + + # | Frame(wan -> lan_zone, IPv4(ip, TCP|UDP)) if (proto, dport) in forwards -> Allow + # Membership test only — the actual DNAT is done in nat_prerouting. + meta iifname "wan" meta oifname { "lan", "wg0" } \ + meta nfproto ipv4 \ + meta l4proto { tcp, udp } \ + meta l4proto . th dport @forwards \ + accept + + # | _ -> Drop (chain policy) + } + + # ── policy output ──────────────────────────────────────────────────────── + # hook = Output, table = Filter, priority = filter (0), default = accept + + chain output { + type filter hook output priority filter; policy accept; + # | _ -> Allow (chain policy) + } + + # ── policy nat_prerouting ──────────────────────────────────────────────── + # hook = Prerouting, table = NAT, priority = dstnat (-100), default = accept + + chain nat_prerouting { + type nat hook prerouting priority dstnat; policy accept; + + # | Frame(_, IPv4(ip, TCP|UDP)) -> + # if FIB.daddrLocal(ip.dst) then DNATMap((proto, dport), forwards) else Allow + meta nfproto ipv4 meta l4proto { tcp, udp } \ + fib daddr type local \ + dnat ip to meta l4proto . th dport map @forwards + + # | _ -> Allow (chain policy) + } + + # ── policy nat_postrouting ─────────────────────────────────────────────── + # hook = Postrouting, table = NAT, priority = srcnat (100), default = accept + + chain nat_postrouting { + type nat hook postrouting priority srcnat; policy accept; + + # | Frame(_ -> wan, IPv4(ip, _)) if ip.src in rfc1918 -> Masquerade + meta oifname "wan" meta nfproto ipv4 ip saddr @rfc1918 masquerade + + # | _ -> Allow (chain policy) + } +} diff --git a/examples/simple-router.fwl b/examples/simple-router.fwl new file mode 100644 index 0000000..9f1c454 --- /dev/null +++ b/examples/simple-router.fwl @@ -0,0 +1,69 @@ +interface wan : WAN { dynamic; }; +interface lan : LAN { cidr4 = { 10.0.0.0/24 }; }; + +zone lan_zone = { lan }; + +let rfc1918 : Set = { 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 }; + +-- Single IPv4 port forward: tcp:8080 -> 10.0.0.10:80 +let forwards : Map<(Protocol, Port), (IP, Port)> = { + (tcp, :8080) -> (10.0.0.10, :80) +}; + +-- Open inbound ports on the router itself +let open_ports : Set = { :22 }; + +-- IPv6 forwarded destination: tcp . 2001:db8::1 . 22000 +let forwards_v6 : Set<(Protocol, IP, Port)> = { + (tcp, 2001:db8::1, :22000) +}; + +policy input : Frame + on { hook = Input, table = Filter, priority = Filter } + = { + | _ if ct.state in { Established, Related } -> Allow; + | Frame(lo, _) -> Allow; + | Frame(_, IPv6(ip6, ICMPv6(_, _))) + if ip6.src in fe80::/10 -> Allow; + | Frame(_, IPv4(_, TCP(tcp, _))) + if tcp.dport in open_ports -> Allow; + | Frame(_, IPv4(_, UDP(udp, _))) + if udp.dport == :51944 -> Allow; + | _ -> Drop; + }; + +policy forward : Frame + on { hook = Forward, table = Filter, priority = Filter } + = { + | _ if ct.state in { Established, Related } -> Allow; + | _ if ct.status == DNAT -> Allow; + | Frame(iif in lan_zone -> wan, _) -> Allow; + | Frame(wan -> iif in lan_zone, IPv4(ip, TCP(th, _) | UDP(th, _))) + if (ip.protocol, th.dport) in forwards -> Allow; + | Frame(wan -> iif in lan_zone, IPv6(ip6, TCP(th, _) | UDP(th, _))) + if (ip6.protocol, ip6.dst, th.dport) in forwards_v6 -> Allow; + | _ -> Drop; + }; + +policy output : Frame + on { hook = Output, table = Filter, priority = Filter } + = { + | _ -> Allow; + }; + +policy nat_prerouting : Frame + on { hook = Prerouting, table = NAT, priority = DstNat } + = { + | Frame(_, IPv4(ip, TCP(th, _) | UDP(th, _))) -> + if perform FIB.daddrLocal(ip.dst) + then DNATMap((ip.protocol, th.dport), forwards) + else Allow; + | _ -> Allow; + }; + +policy nat_postrouting : Frame + on { hook = Postrouting, table = NAT, priority = SrcNat } + = { + | Frame(_ -> wan, IPv4(ip, _)) if ip.src in rfc1918 -> Masquerade; + | _ -> Allow; + }; diff --git a/examples/simple-router.fwl.json b/examples/simple-router.fwl.json new file mode 100644 index 0000000..02a6dce --- /dev/null +++ b/examples/simple-router.fwl.json @@ -0,0 +1,955 @@ +{ + "nftables": [ + { + "metainfo": { + "json_schema_version": 1 + } + }, + { + "table": { + "family": "inet", + "name": "fwl" + } + }, + { + "chain": { + "family": "inet", + "hook": "input", + "name": "input", + "policy": "drop", + "prio": 0, + "table": "fwl", + "type": "filter" + } + }, + { + "chain": { + "family": "inet", + "hook": "forward", + "name": "forward", + "policy": "drop", + "prio": 0, + "table": "fwl", + "type": "filter" + } + }, + { + "chain": { + "family": "inet", + "hook": "output", + "name": "output", + "policy": "accept", + "prio": 0, + "table": "fwl", + "type": "filter" + } + }, + { + "chain": { + "family": "inet", + "hook": "prerouting", + "name": "nat_prerouting", + "policy": "accept", + "prio": -100, + "table": "fwl", + "type": "nat" + } + }, + { + "chain": { + "family": "inet", + "hook": "postrouting", + "name": "nat_postrouting", + "policy": "accept", + "prio": 100, + "table": "fwl", + "type": "nat" + } + }, + { + "set": { + "elem": [ + { + "prefix": { + "addr": "10.0.0.0", + "len": 8 + } + }, + { + "prefix": { + "addr": "172.16.0.0", + "len": 12 + } + }, + { + "prefix": { + "addr": "192.168.0.0", + "len": 16 + } + } + ], + "family": "inet", + "name": "rfc1918", + "table": "fwl", + "type": "ipv4_addr" + } + }, + { + "map": { + "elem": [ + [ + { + "concat": [ + "tcp", + 8080 + ] + }, + { + "concat": [ + "10.0.0.10", + 80 + ] + } + ] + ], + "family": "inet", + "map": [ + "ipv4_addr", + "inet_service" + ], + "name": "forwards", + "table": "fwl", + "type": [ + "inet_proto", + "inet_service" + ] + } + }, + { + "set": { + "elem": [ + 22 + ], + "family": "inet", + "name": "open_ports", + "table": "fwl", + "type": "inet_service" + } + }, + { + "set": { + "elem": [ + { + "concat": [ + "tcp", + "2001:db8:0:0:0:0:0:1", + 22000 + ] + } + ], + "family": "inet", + "name": "forwards_v6", + "table": "fwl", + "type": [ + "inet_proto", + "ipv4_addr", + "inet_service" + ] + } + }, + { + "rule": { + "chain": "input", + "expr": [ + { + "match": { + "left": { + "ct": { + "key": "state" + } + }, + "op": "in", + "right": [ + "established", + "related" + ] + } + }, + { + "accept": null + } + ], + "family": "inet", + "table": "fwl" + } + }, + { + "rule": { + "chain": "input", + "expr": [ + { + "match": { + "left": { + "meta": { + "key": "iifname" + } + }, + "op": "==", + "right": "lo" + } + }, + { + "accept": null + } + ], + "family": "inet", + "table": "fwl" + } + }, + { + "rule": { + "chain": "input", + "expr": [ + { + "match": { + "left": { + "meta": { + "key": "nfproto" + } + }, + "op": "==", + "right": "ipv6" + } + }, + { + "match": { + "left": { + "payload": { + "field": "nexthdr", + "protocol": "ip6" + } + }, + "op": "==", + "right": "ipv6-icmp" + } + }, + { + "match": { + "left": { + "payload": { + "field": "saddr", + "protocol": "ip6" + } + }, + "op": "==", + "right": { + "prefix": { + "addr": "fe80:0:0:0:0:0:0:0", + "len": 10 + } + } + } + }, + { + "accept": null + } + ], + "family": "inet", + "table": "fwl" + } + }, + { + "rule": { + "chain": "input", + "expr": [ + { + "match": { + "left": { + "meta": { + "key": "nfproto" + } + }, + "op": "==", + "right": "ipv4" + } + }, + { + "match": { + "left": { + "meta": { + "key": "l4proto" + } + }, + "op": "==", + "right": "tcp" + } + }, + { + "match": { + "left": { + "payload": { + "field": "dport", + "protocol": "tcp" + } + }, + "op": "==", + "right": "@open_ports" + } + }, + { + "accept": null + } + ], + "family": "inet", + "table": "fwl" + } + }, + { + "rule": { + "chain": "input", + "expr": [ + { + "match": { + "left": { + "meta": { + "key": "nfproto" + } + }, + "op": "==", + "right": "ipv4" + } + }, + { + "match": { + "left": { + "meta": { + "key": "l4proto" + } + }, + "op": "==", + "right": "udp" + } + }, + { + "match": { + "left": { + "payload": { + "field": "dport", + "protocol": "udp" + } + }, + "op": "==", + "right": "51944" + } + }, + { + "accept": null + } + ], + "family": "inet", + "table": "fwl" + } + }, + { + "rule": { + "chain": "input", + "expr": [ + { + "drop": null + } + ], + "family": "inet", + "table": "fwl" + } + }, + { + "rule": { + "chain": "forward", + "expr": [ + { + "match": { + "left": { + "ct": { + "key": "state" + } + }, + "op": "in", + "right": [ + "established", + "related" + ] + } + }, + { + "accept": null + } + ], + "family": "inet", + "table": "fwl" + } + }, + { + "rule": { + "chain": "forward", + "expr": [ + { + "match": { + "left": { + "ct": { + "key": "status" + } + }, + "op": "==", + "right": "dnat" + } + }, + { + "accept": null + } + ], + "family": "inet", + "table": "fwl" + } + }, + { + "rule": { + "chain": "forward", + "expr": [ + { + "match": { + "left": { + "meta": { + "key": "iifname" + } + }, + "op": "in", + "right": { + "set": [ + "lan" + ] + } + } + }, + { + "match": { + "left": { + "meta": { + "key": "oifname" + } + }, + "op": "==", + "right": "wan" + } + }, + { + "accept": null + } + ], + "family": "inet", + "table": "fwl" + } + }, + { + "rule": { + "chain": "forward", + "expr": [ + { + "match": { + "left": { + "meta": { + "key": "iifname" + } + }, + "op": "==", + "right": "wan" + } + }, + { + "match": { + "left": { + "meta": { + "key": "oifname" + } + }, + "op": "in", + "right": { + "set": [ + "lan" + ] + } + } + }, + { + "match": { + "left": { + "meta": { + "key": "nfproto" + } + }, + "op": "==", + "right": "ipv4" + } + }, + { + "match": { + "left": { + "meta": { + "key": "l4proto" + } + }, + "op": "==", + "right": "tcp" + } + }, + { + "match": { + "left": { + "concat": [ + { + "payload": { + "field": "protocol", + "protocol": "ip" + } + }, + { + "payload": { + "field": "dport", + "protocol": "th" + } + } + ] + }, + "op": "==", + "right": "@forwards" + } + }, + { + "accept": null + } + ], + "family": "inet", + "table": "fwl" + } + }, + { + "rule": { + "chain": "forward", + "expr": [ + { + "match": { + "left": { + "meta": { + "key": "iifname" + } + }, + "op": "==", + "right": "wan" + } + }, + { + "match": { + "left": { + "meta": { + "key": "oifname" + } + }, + "op": "in", + "right": { + "set": [ + "lan" + ] + } + } + }, + { + "match": { + "left": { + "meta": { + "key": "nfproto" + } + }, + "op": "==", + "right": "ipv4" + } + }, + { + "match": { + "left": { + "meta": { + "key": "l4proto" + } + }, + "op": "==", + "right": "udp" + } + }, + { + "match": { + "left": { + "concat": [ + { + "payload": { + "field": "protocol", + "protocol": "ip" + } + }, + { + "payload": { + "field": "dport", + "protocol": "th" + } + } + ] + }, + "op": "==", + "right": "@forwards" + } + }, + { + "accept": null + } + ], + "family": "inet", + "table": "fwl" + } + }, + { + "rule": { + "chain": "forward", + "expr": [ + { + "match": { + "left": { + "meta": { + "key": "iifname" + } + }, + "op": "==", + "right": "wan" + } + }, + { + "match": { + "left": { + "meta": { + "key": "oifname" + } + }, + "op": "in", + "right": { + "set": [ + "lan" + ] + } + } + }, + { + "match": { + "left": { + "meta": { + "key": "nfproto" + } + }, + "op": "==", + "right": "ipv6" + } + }, + { + "match": { + "left": { + "meta": { + "key": "l4proto" + } + }, + "op": "==", + "right": "tcp" + } + }, + { + "match": { + "left": { + "concat": [ + { + "payload": { + "field": "protocol", + "protocol": "ip6" + } + }, + { + "payload": { + "field": "daddr", + "protocol": "ip6" + } + }, + { + "payload": { + "field": "dport", + "protocol": "th" + } + } + ] + }, + "op": "==", + "right": "@forwards_v6" + } + }, + { + "accept": null + } + ], + "family": "inet", + "table": "fwl" + } + }, + { + "rule": { + "chain": "forward", + "expr": [ + { + "match": { + "left": { + "meta": { + "key": "iifname" + } + }, + "op": "==", + "right": "wan" + } + }, + { + "match": { + "left": { + "meta": { + "key": "oifname" + } + }, + "op": "in", + "right": { + "set": [ + "lan" + ] + } + } + }, + { + "match": { + "left": { + "meta": { + "key": "nfproto" + } + }, + "op": "==", + "right": "ipv6" + } + }, + { + "match": { + "left": { + "meta": { + "key": "l4proto" + } + }, + "op": "==", + "right": "udp" + } + }, + { + "match": { + "left": { + "concat": [ + { + "payload": { + "field": "protocol", + "protocol": "ip6" + } + }, + { + "payload": { + "field": "daddr", + "protocol": "ip6" + } + }, + { + "payload": { + "field": "dport", + "protocol": "th" + } + } + ] + }, + "op": "==", + "right": "@forwards_v6" + } + }, + { + "accept": null + } + ], + "family": "inet", + "table": "fwl" + } + }, + { + "rule": { + "chain": "forward", + "expr": [ + { + "drop": null + } + ], + "family": "inet", + "table": "fwl" + } + }, + { + "rule": { + "chain": "output", + "expr": [ + { + "accept": null + } + ], + "family": "inet", + "table": "fwl" + } + }, + { + "rule": { + "chain": "nat_prerouting", + "expr": [ + { + "match": { + "left": { + "meta": { + "key": "nfproto" + } + }, + "op": "==", + "right": "ipv4" + } + }, + { + "match": { + "left": { + "meta": { + "key": "l4proto" + } + }, + "op": "==", + "right": "tcp" + } + }, + { + "accept": null + } + ], + "family": "inet", + "table": "fwl" + } + }, + { + "rule": { + "chain": "nat_prerouting", + "expr": [ + { + "match": { + "left": { + "meta": { + "key": "nfproto" + } + }, + "op": "==", + "right": "ipv4" + } + }, + { + "match": { + "left": { + "meta": { + "key": "l4proto" + } + }, + "op": "==", + "right": "udp" + } + }, + { + "accept": null + } + ], + "family": "inet", + "table": "fwl" + } + }, + { + "rule": { + "chain": "nat_prerouting", + "expr": [ + { + "accept": null + } + ], + "family": "inet", + "table": "fwl" + } + }, + { + "rule": { + "chain": "nat_postrouting", + "expr": [ + { + "match": { + "left": { + "meta": { + "key": "oifname" + } + }, + "op": "==", + "right": "wan" + } + }, + { + "match": { + "left": { + "meta": { + "key": "nfproto" + } + }, + "op": "==", + "right": "ipv4" + } + }, + { + "match": { + "left": { + "payload": { + "field": "saddr", + "protocol": "ip" + } + }, + "op": "==", + "right": "@rfc1918" + } + }, + { + "masquerade": null + } + ], + "family": "inet", + "table": "fwl" + } + }, + { + "rule": { + "chain": "nat_postrouting", + "expr": [ + { + "accept": null + } + ], + "family": "inet", + "table": "fwl" + } + } + ] +} diff --git a/examples/simple-router.nft b/examples/simple-router.nft new file mode 100644 index 0000000..bf00644 --- /dev/null +++ b/examples/simple-router.nft @@ -0,0 +1,98 @@ +table inet fwl { + + # ── let rfc1918 ────────────────────────────────────────────────────────── + set rfc1918 { + type ipv4_addr + flags interval + elements = { + 10.0.0.0/8, + 172.16.0.0/12, + 192.168.0.0/16 + } + } + + # ── let open_ports : Set ─────────────────────────────────────────── + set open_ports { + type inet_service + elements = { 22 } + } + + # ── let forwards_v6 : Set<(Protocol, IP, Port)> ────────────────────────── + set forwards_v6 { + type inet_proto . ipv6_addr . inet_service + elements = { + tcp . 2001:db8::1 . 22000 + } + } + + # ── let forwards : Map<(Protocol, Port), (IP, Port)> ──────────────────── + map forwards { + type inet_proto . inet_service : ipv4_addr . inet_service + elements = { + tcp . 8080 : 10.0.0.10 . 80 + } + } + + # ── zone lan_zone = { lan } ────────────────────────────────────────────── + # Zones compile to anonymous sets wherever referenced in iifname/oifname. + # With a single member the set degenerates to a plain string match, + # but we keep the set form so the compiler output is uniform regardless + # of zone size. + set lan_zone { + type ifname + elements = { "lan" } + } + + # ── policy input ───────────────────────────────────────────────────────── + chain input { + type filter hook input priority filter; policy drop; + + ct state { established, related } accept + iifname "lo" accept + meta nfproto ipv6 ip6 nexthdr ipv6-icmp ip6 saddr fe80::/10 accept + meta nfproto ipv4 meta l4proto tcp tcp dport @open_ports accept + meta nfproto ipv4 meta l4proto udp udp dport 51944 accept + } + + # ── policy forward ─────────────────────────────────────────────────────── + chain forward { + type filter hook forward priority filter; policy drop; + + ct state { established, related } accept + ct status dnat accept + + # | Frame(iif in lan_zone -> wan, _) -> Allow + meta iifname @lan_zone meta oifname "wan" accept + + # | Frame(wan -> iif in lan_zone, IPv4 TCP|UDP) if (proto,dport) in forwards + meta iifname "wan" meta oifname @lan_zone \ + meta nfproto ipv4 meta l4proto { tcp, udp } \ + meta l4proto . th dport @forwards accept + + # | Frame(wan -> iif in lan_zone, IPv6 TCP|UDP) if (proto,dst,dport) in forwards_v6 + meta iifname "wan" meta oifname @lan_zone \ + meta nfproto ipv6 meta l4proto { tcp, udp } \ + meta l4proto . ip6 daddr . th dport @forwards_v6 accept + } + + # ── policy output ──────────────────────────────────────────────────────── + chain output { + type filter hook output priority filter; policy accept; + } + + # ── policy nat_prerouting ──────────────────────────────────────────────── + chain nat_prerouting { + type nat hook prerouting priority dstnat; policy accept; + + meta nfproto ipv4 meta l4proto { tcp, udp } \ + fib daddr type local \ + dnat ip to meta l4proto . th dport map @forwards + } + + # ── policy nat_postrouting ─────────────────────────────────────────────── + chain nat_postrouting { + type nat hook postrouting priority srcnat; policy accept; + + meta oifname "wan" meta nfproto ipv4 ip saddr @rfc1918 masquerade + } +} diff --git a/examples/simple-router.nft.json b/examples/simple-router.nft.json new file mode 100644 index 0000000..e324ce5 --- /dev/null +++ b/examples/simple-router.nft.json @@ -0,0 +1,693 @@ +{ + "nftables": [ + { + "metainfo": { + "version": "1.1.6", + "release_name": "Commodore Bullmoose #7", + "json_schema_version": 1 + } + }, + { + "table": { + "family": "inet", + "name": "fwl" + } + }, + { + "chain": { + "family": "inet", + "table": "fwl", + "name": "input", + "type": "filter", + "hook": "input", + "prio": 0, + "policy": "drop" + } + }, + { + "chain": { + "family": "inet", + "table": "fwl", + "name": "forward", + "type": "filter", + "hook": "forward", + "prio": 0, + "policy": "drop" + } + }, + { + "chain": { + "family": "inet", + "table": "fwl", + "name": "output", + "type": "filter", + "hook": "output", + "prio": 0, + "policy": "accept" + } + }, + { + "chain": { + "family": "inet", + "table": "fwl", + "name": "nat_prerouting", + "type": "nat", + "hook": "prerouting", + "prio": -100, + "policy": "accept" + } + }, + { + "chain": { + "family": "inet", + "table": "fwl", + "name": "nat_postrouting", + "type": "nat", + "hook": "postrouting", + "prio": 100, + "policy": "accept" + } + }, + { + "set": { + "family": "inet", + "name": "rfc1918", + "table": "fwl", + "type": "ipv4_addr", + "flags": [ + "interval" + ], + "elem": [ + { + "prefix": { + "addr": "10.0.0.0", + "len": 8 + } + }, + { + "prefix": { + "addr": "172.16.0.0", + "len": 12 + } + }, + { + "prefix": { + "addr": "192.168.0.0", + "len": 16 + } + } + ] + } + }, + { + "set": { + "family": "inet", + "name": "open_ports", + "table": "fwl", + "type": "inet_service", + "elem": [ + 22 + ] + } + }, + { + "set": { + "family": "inet", + "name": "forwards_v6", + "table": "fwl", + "type": [ + "inet_proto", + "ipv6_addr", + "inet_service" + ], + "elem": [ + { + "concat": [ + "tcp", + "2001:db8::1", + 22000 + ] + } + ] + } + }, + { + "map": { + "family": "inet", + "name": "forwards", + "table": "fwl", + "type": [ + "inet_proto", + "inet_service" + ], + "map": [ + "ipv4_addr", + "inet_service" + ], + "elem": [ + [ + { + "concat": [ + "tcp", + 8080 + ] + }, + { + "concat": [ + "10.0.0.10", + 80 + ] + } + ] + ] + } + }, + { + "set": { + "family": "inet", + "name": "lan_zone", + "table": "fwl", + "type": "ifname", + "elem": [ + "lan" + ] + } + }, + { + "rule": { + "family": "inet", + "table": "fwl", + "chain": "input", + "expr": [ + { + "match": { + "op": "==", + "left": { + "ct": { + "key": "state" + } + }, + "right": { + "set": [ + "established", + "related" + ] + } + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "inet", + "table": "fwl", + "chain": "input", + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "iifname" + } + }, + "right": "lo" + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "inet", + "table": "fwl", + "chain": "input", + "expr": [ + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "ip6", + "field": "nexthdr" + } + }, + "right": "ipv6-icmp" + } + }, + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "ip6", + "field": "saddr" + } + }, + "right": { + "prefix": { + "addr": "fe80::", + "len": 10 + } + } + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "inet", + "table": "fwl", + "chain": "input", + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "nfproto" + } + }, + "right": "ipv4" + } + }, + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "tcp", + "field": "dport" + } + }, + "right": "@open_ports" + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "inet", + "table": "fwl", + "chain": "input", + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "nfproto" + } + }, + "right": "ipv4" + } + }, + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "udp", + "field": "dport" + } + }, + "right": 51944 + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "inet", + "table": "fwl", + "chain": "forward", + "expr": [ + { + "match": { + "op": "==", + "left": { + "ct": { + "key": "state" + } + }, + "right": { + "set": [ + "established", + "related" + ] + } + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "inet", + "table": "fwl", + "chain": "forward", + "expr": [ + { + "match": { + "op": "in", + "left": { + "ct": { + "key": "status" + } + }, + "right": "dnat" + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "inet", + "table": "fwl", + "chain": "forward", + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "iifname" + } + }, + "right": "@lan_zone" + } + }, + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "oifname" + } + }, + "right": "wan" + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "inet", + "table": "fwl", + "chain": "forward", + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "iifname" + } + }, + "right": "wan" + } + }, + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "oifname" + } + }, + "right": "@lan_zone" + } + }, + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "nfproto" + } + }, + "right": "ipv4" + } + }, + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "l4proto" + } + }, + "right": { + "set": [ + "tcp", + "udp" + ] + } + } + }, + { + "match": { + "op": "==", + "left": { + "concat": [ + { + "meta": { + "key": "l4proto" + } + }, + { + "payload": { + "protocol": "th", + "field": "dport" + } + } + ] + }, + "right": "@forwards" + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "inet", + "table": "fwl", + "chain": "forward", + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "iifname" + } + }, + "right": "wan" + } + }, + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "oifname" + } + }, + "right": "@lan_zone" + } + }, + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "l4proto" + } + }, + "right": { + "set": [ + "tcp", + "udp" + ] + } + } + }, + { + "match": { + "op": "==", + "left": { + "concat": [ + { + "meta": { + "key": "l4proto" + } + }, + { + "payload": { + "protocol": "ip6", + "field": "daddr" + } + }, + { + "payload": { + "protocol": "th", + "field": "dport" + } + } + ] + }, + "right": "@forwards_v6" + } + }, + { + "accept": null + } + ] + } + }, + { + "rule": { + "family": "inet", + "table": "fwl", + "chain": "nat_prerouting", + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "nfproto" + } + }, + "right": "ipv4" + } + }, + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "l4proto" + } + }, + "right": { + "set": [ + "tcp", + "udp" + ] + } + } + }, + { + "match": { + "op": "==", + "left": { + "fib": { + "result": "type", + "flags": [ + "daddr" + ] + } + }, + "right": "local" + } + }, + { + "dnat": { + "family": "ip", + "addr": { + "map": { + "key": { + "concat": [ + { + "meta": { + "key": "l4proto" + } + }, + { + "payload": { + "protocol": "th", + "field": "dport" + } + } + ] + }, + "data": "@forwards" + } + } + } + } + ] + } + }, + { + "rule": { + "family": "inet", + "table": "fwl", + "chain": "nat_postrouting", + "expr": [ + { + "match": { + "op": "==", + "left": { + "meta": { + "key": "oifname" + } + }, + "right": "wan" + } + }, + { + "match": { + "op": "==", + "left": { + "payload": { + "protocol": "ip", + "field": "saddr" + } + }, + "right": "@rfc1918" + } + }, + { + "masquerade": null + } + ] + } + } + ] +} \ No newline at end of file