Import nftables maps: one port forward or rule per element

A DNAT map (named or anonymous) becomes one port forward per element,
named after the map; a verdict map becomes one rule per element, in
place, with jumps inlined. A lookup matches one element at most, so the
expanded rules do the same. A named map that can't be expanded (keyed by
several fields) is left out with a note instead of a raw rule naming it,
which nftables would refuse. Test fixture made with nft 1.1.3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-19 10:24:28 +01:00
1 parent eaadbe6a3d
commit ebec2cfcca
7 files changed
+340 -4

No files matched your search

+2 -1
View File
@@ -52,7 +52,8 @@ the way Jarl and Steward are.
- **Import** of an existing nftables ruleset (`hc import`): rules keep - **Import** of an existing nftables ruleset (`hc import`): rules keep
their order, defines name the sets, DNAT becomes port forwards, what their order, defines name the sets, DNAT becomes port forwards, what
can't be translated stays as raw rules, with notes; `hc takeover` can't be translated stays as raw rules, with notes; `hc takeover`
deletes the old table, on probation (docs/import.md). deletes the old table, on probation (docs/import.md). DNAT maps and
verdict maps are expanded into one port forward or rule per element.
- **The `hc` CLI, the REST API and the web UI** (dashboard, rules per - **The `hc` CLI, the REST API and the web UI** (dashboard, rules per
chain with hits and reordering, NAT, zones, sets, services, the log, chain with hits and reordering, NAT, zones, sets, services, the log,
settings, import, history with undo), with users, roles, scoped API settings, import, history with undo), with users, roles, scoped API
+9
View File
@@ -57,6 +57,15 @@ Import a ruleset.
become part of the port forward, and a masquerade for inside sources become part of the port forward, and a masquerade for inside sources
to the same host becomes its hairpin. An IPv6 accept to the same port to the same host becomes its hairpin. An IPv6 accept to the same port
becomes the forward's IPv6 pinhole when there is only one candidate. becomes the forward's IPv6 pinhole when there is only one candidate.
* **Maps are expanded**, one element at a time. A DNAT map
(`dnat ip to tcp dport map @forwards`, named or written in the rule)
becomes one port forward per element; forwards from a named map are
called after it (`forwards-443`). A verdict map
(`tcp dport vmap { 22 : accept, 23 : drop }`) becomes one rule per
element, in place, with `jump`s inlined as above. A lookup matches at
most one element, so the rules do the same. A map keyed by several
fields (`ip saddr . tcp dport`) can't be expanded: a rule using a named
one is left out with a note, to be added by hand.
* **Masquerade and SNAT** become source NAT; **redirect** becomes a * **Masquerade and SNAT** become source NAT; **redirect** becomes a
redirect. redirect.
* **Policies** become the chains' policies. * **Policies** become the chains' policies.
+182 -3
View File
@@ -81,6 +81,16 @@ type nftSet struct {
Elem []json.RawMessage `json:"elem"` Elem []json.RawMessage `json:"elem"`
} }
// nftMap is a named map: its elements are [key, value] pairs.
type nftMap struct {
Family string `json:"family"`
Table string `json:"table"`
Name string `json:"name"`
Type json.RawMessage `json:"type"`
Map json.RawMessage `json:"map"`
Elem []json.RawMessage `json:"elem"`
}
type nftFlowtable struct { type nftFlowtable struct {
Table string `json:"table"` Table string `json:"table"`
Name string `json:"name"` Name string `json:"name"`
@@ -93,6 +103,7 @@ type importer struct {
chains map[string]nftChain chains map[string]nftChain
rules map[string][]nftRule // by chain, in order rules map[string][]nftRule // by chain, in order
sets map[string]nftSet sets map[string]nftSet
maps map[string]nftMap
flow []nftFlowtable flow []nftFlowtable
text map[int]string // rule text by handle text map[int]string // rule text by handle
st *model.State st *model.State
@@ -120,7 +131,7 @@ func Import(in Input) (*Result, error) {
if err := json.Unmarshal(in.JSON, &doc); err != nil { if err := json.Unmarshal(in.JSON, &doc); err != nil {
return nil, fmt.Errorf("the input isn't nft's JSON (nft -j list ruleset): %v", err) return nil, fmt.Errorf("the input isn't nft's JSON (nft -j list ruleset): %v", err)
} }
im := &importer{in: in, chains: map[string]nftChain{}, rules: map[string][]nftRule{}, sets: map[string]nftSet{}, im := &importer{in: in, chains: map[string]nftChain{}, rules: map[string][]nftRule{}, sets: map[string]nftSet{}, maps: map[string]nftMap{},
text: map[int]string{}, st: model.NewState(), used: map[string]bool{}, noted: map[string]bool{}, positions: map[string]float64{}} text: map[int]string{}, st: model.NewState(), used: map[string]bool{}, noted: map[string]bool{}, positions: map[string]float64{}}
var tables []nftTable var tables []nftTable
for _, item := range doc.Nftables { for _, item := range doc.Nftables {
@@ -172,6 +183,12 @@ func Import(in Input) (*Result, error) {
if ours(s.Family, s.Table) { if ours(s.Family, s.Table) {
im.sets[s.Name] = s im.sets[s.Name] = s
} }
case item["map"] != nil:
var m nftMap
json.Unmarshal(item["map"], &m)
if ours(m.Family, m.Table) {
im.maps[m.Name] = m
}
case item["flowtable"] != nil: case item["flowtable"] != nil:
var f nftFlowtable var f nftFlowtable
json.Unmarshal(item["flowtable"], &f) json.Unmarshal(item["flowtable"], &f)
@@ -465,6 +482,20 @@ type parsed struct {
unsupported []string unsupported []string
comment string comment string
handle int handle int
// mapUse: a dnat to a map, or a verdict map, still to be expanded
// into one rule per element (expandMaps).
mapUse *mapUse
// name: a port forward's name, when the rule's comment won't do
// (one of a map's elements).
name string
}
// mapUse is a lookup in a map: "dnat ip to tcp dport map @forwards" or
// "tcp dport vmap { 22 : accept }".
type mapUse struct {
kind string // dnat or vmap
key json.RawMessage // what is looked up: {"payload": ...}, {"meta": ...}
data json.RawMessage // "@name", or {"set": [[key, value], ...]}
} }
type match struct { type match struct {
@@ -536,6 +567,16 @@ func (p *parsed) expr(k string, v json.RawMessage) {
Port json.RawMessage `json:"port"` Port json.RawMessage `json:"port"`
} }
json.Unmarshal(v, &x) json.Unmarshal(v, &x)
var lookup struct {
Map *struct {
Key json.RawMessage `json:"key"`
Data json.RawMessage `json:"data"`
} `json:"map"`
}
if k == "dnat" && json.Unmarshal(x.Addr, &lookup) == nil && lookup.Map != nil && len(x.Port) == 0 {
p.mapUse = &mapUse{kind: "dnat", key: lookup.Map.Key, data: lookup.Map.Data}
return
}
addr, port := values(x.Addr), values(x.Port) addr, port := values(x.Addr), values(x.Port)
if len(addr) != 1 || len(port) > 1 { if len(addr) != 1 || len(port) > 1 {
p.bad(k + " to a range or map") p.bad(k + " to a range or map")
@@ -572,6 +613,13 @@ func (p *parsed) expr(k string, v json.RawMessage) {
} }
case "flow": case "flow":
p.flowAdd = true p.flowAdd = true
case "vmap":
var x struct {
Key json.RawMessage `json:"key"`
Data json.RawMessage `json:"data"`
}
json.Unmarshal(v, &x)
p.mapUse = &mapUse{kind: "vmap", key: x.Key, data: x.Data}
default: default:
p.bad(k) p.bad(k)
} }
@@ -740,8 +788,11 @@ func (im *importer) run() {
// target chain gets the jump's own matches in front. // target chain gets the jump's own matches in front.
func (im *importer) flatten(chain string, prefix *parsed, depth int) []*parsed { func (im *importer) flatten(chain string, prefix *parsed, depth int) []*parsed {
var out []*parsed var out []*parsed
var rules []*parsed
for _, r := range im.rules[chain] { for _, r := range im.rules[chain] {
p := parse(r) rules = append(rules, im.expandMaps(parse(r))...)
}
for _, p := range rules {
if prefix != nil { if prefix != nil {
p = im.merge(prefix, p) p = im.merge(prefix, p)
} }
@@ -770,6 +821,115 @@ func (im *importer) flatten(chain string, prefix *parsed, depth int) []*parsed {
return out return out
} }
// expandMaps turns a rule that looks something up in a map into one rule
// per element, each matching the element's key and doing what its value
// says: "tcp dport vmap { 22 : accept, 23 : drop }" becomes "tcp dport 22
// accept" and "tcp dport 23 drop"; "dnat ip to tcp dport map @forwards"
// one dnat (later a port forward) per element. A lookup finds at most one
// element, and their keys don't overlap, so the rules do the same in
// order. A map that can't be expanded leaves the rule unsupported.
func (im *importer) expandMaps(p *parsed) []*parsed {
u := p.mapUse
if u == nil {
return []*parsed{p}
}
p.mapUse = nil
fail := func(why string) []*parsed {
p.bad(why)
return []*parsed{p}
}
var keyObj map[string]json.RawMessage
json.Unmarshal(u.key, &keyObj)
if keyObj["concat"] != nil {
return fail("a map keyed by several fields (" + u.kind + ")")
}
// The elements: a named map's, or the anonymous one's.
var elems []json.RawMessage
mapName := ""
var ref string
if json.Unmarshal(u.data, &ref) == nil && strings.HasPrefix(ref, "@") {
m, ok := im.maps[ref[1:]]
if !ok {
return fail("the map " + ref + ", which isn't in the table")
}
mapName, elems = m.Name, m.Elem
} else {
var anon struct {
Set []json.RawMessage `json:"set"`
}
if json.Unmarshal(u.data, &anon) != nil || anon.Set == nil {
return fail("a " + u.kind + " map Housecarl can't read")
}
elems = anon.Set
}
if len(elems) == 0 {
im.note("rule handle %d looks up the empty map %s: left out", p.handle, map[bool]string{true: "@" + mapName, false: "{}"}[mapName != ""])
return nil
}
var out []*parsed
for _, e := range elems {
var pair []json.RawMessage
if json.Unmarshal(e, &pair) != nil || len(pair) != 2 {
return fail("a map element Housecarl can't read")
}
q := *p
q.unsupported = append([]string{}, p.unsupported...)
q.match(match{Op: "==", Left: u.key, Right: pair[0]})
switch u.kind {
case "dnat":
addr, port := mapTarget(pair[1])
if addr == "" {
return fail("a dnat map whose values aren't addresses (or address . port)")
}
q.dnatAddr, q.dnatPort = addr, port
if mapName != "" {
if k := values(pair[0]); len(k) == 1 {
q.name = mapName + "-" + k[0]
}
}
case "vmap":
var verdict map[string]json.RawMessage
if json.Unmarshal(pair[1], &verdict) != nil || len(verdict) != 1 {
return fail("a verdict map value Housecarl can't read")
}
for k, v := range verdict {
q.expr(k, v)
}
}
out = append(out, &q)
}
if mapName != "" {
what := map[string]string{"dnat": "port forward", "vmap": "rule"}[u.kind]
if !im.noted["map "+mapName] {
im.noted["map "+mapName] = true
if len(elems) == 1 {
im.note("map %s: its element became a %s", mapName, what)
} else {
im.note("map %s: its %d elements became %d %ss, one each", mapName, len(elems), len(elems), what)
}
}
}
return out
}
// mapTarget reads a dnat map's value: an address, or address . port.
func mapTarget(raw json.RawMessage) (addr, port string) {
var obj struct {
Concat []json.RawMessage `json:"concat"`
}
if json.Unmarshal(raw, &obj) == nil && len(obj.Concat) == 2 {
a, b := values(obj.Concat[0]), values(obj.Concat[1])
if len(a) == 1 && len(b) == 1 {
return a[0], b[0]
}
return "", ""
}
if a := values(raw); len(a) == 1 {
return a[0], ""
}
return "", ""
}
// hasMatch reports whether p matches more than the jump's own matches. // hasMatch reports whether p matches more than the jump's own matches.
func hasMatch(p, prefix *parsed) bool { func hasMatch(p, prefix *parsed) bool {
return len(p.dports) != len(prefix.dports) || len(p.src) != len(prefix.src) || len(p.dst) != len(prefix.dst) || return len(p.dports) != len(prefix.dports) || len(p.src) != len(prefix.src) || len(p.dst) != len(prefix.dst) ||
@@ -1048,7 +1208,12 @@ func (im *importer) rule(chain string, p *parsed) {
case "accept", "drop", "reject": case "accept", "drop", "reject":
r.Action = p.verdict r.Action = p.verdict
case "": case "":
if !p.log { switch {
case len(p.unsupported) > 0:
// The verdict may be in what couldn't be read (a verdict map).
im.rawRule(r, p)
return
case !p.log:
im.note("a %s rule with no verdict (handle %d) was left out", chain, p.handle) im.note("a %s rule with no verdict (handle %d) was left out", chain, p.handle)
return return
} }
@@ -1065,6 +1230,8 @@ func (im *importer) rule(chain string, p *parsed) {
im.rawRule(r, p) im.rawRule(r, p)
} }
var mapRefRe = regexp.MustCompile(`@([A-Za-z0-9_]+)`)
func (im *importer) rawRule(r *model.Rule, p *parsed) { func (im *importer) rawRule(r *model.Rule, p *parsed) {
text, ok := im.text[p.handle] text, ok := im.text[p.handle]
if !ok { if !ok {
@@ -1075,6 +1242,15 @@ func (im *importer) rawRule(r *model.Rule, p *parsed) {
if i := strings.LastIndex(text, ` comment "`); i >= 0 { if i := strings.LastIndex(text, ` comment "`); i >= 0 {
text = strings.TrimSpace(text[:i]) text = strings.TrimSpace(text[:i])
} }
// A named map isn't carried over: a raw rule naming it would make
// nftables refuse the whole ruleset.
for _, ref := range mapRefRe.FindAllStringSubmatch(text, -1) {
if _, ok := im.maps[ref[1]]; ok {
im.note("rule handle %d of chain %s uses the map @%s, which couldn't be expanded (%s): left out, add it by hand: %s",
p.handle, r.Chain, ref[1], strings.Join(p.unsupported, ", "), text)
return
}
}
if r.Action == "" { if r.Action == "" {
r.Action = model.ActionAccept r.Action = model.ActionAccept
} }
@@ -1100,6 +1276,9 @@ func (im *importer) dstnat(p *parsed) {
break break
} }
f := &model.PortForward{Protocols: []string{p.proto}, Ports: ports, To: p.dnatAddr, ToPort: p.dnatPort, Comment: p.comment, Name: forwardName(p.comment)} f := &model.PortForward{Protocols: []string{p.proto}, Ports: ports, To: p.dnatAddr, ToPort: p.dnatPort, Comment: p.comment, Name: forwardName(p.comment)}
if p.name != "" {
f.Name = strings.ToLower(nameRe.ReplaceAllString(p.name, "-"))
}
if f.ToPort == ports { if f.ToPort == ports {
f.ToPort = "" f.ToPort = ""
} }
+66
View File
@@ -1,6 +1,7 @@
package importer package importer
import ( import (
"fmt"
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
@@ -173,3 +174,68 @@ func TestImportedRulesetPassesNft(t *testing.T) {
t.Errorf("nft -c: %v\n%s", err, out) t.Errorf("nft -c: %v\n%s", err, out)
} }
} }
// TestImportMaps: dnat maps become port forwards and verdict maps rules,
// one per element (testdata/maps.nft); a map that can't be expanded is
// left out with a note, never kept as a raw rule naming it.
func TestImportMaps(t *testing.T) {
js, _ := os.ReadFile("testdata/maps.json")
txt, _ := os.ReadFile("testdata/maps.txt")
res, err := Import(Input{JSON: js, Text: string(txt)})
if err != nil {
t.Fatal(err)
}
var fwds []string
for _, f := range res.State.Forwards {
fwds = append(fwds, fmt.Sprintf("%s %s/%s->%s:%s", f.Name, strings.Join(f.Protocols, "+"), f.Ports, f.To, f.ToPort))
}
var rules []string
for _, r := range res.State.Rules {
rules = append(rules, fmt.Sprintf("%s in=%s from=%s %s %s%s", r.Chain, strings.Join(r.InZones, ","), strings.Join(r.Sources, ","),
strings.Join(r.Services, ","), r.Action, r.Raw))
}
want := []string{
"portforwards-80 tcp/80->192.168.1.50:8080",
"portforwards-443 tcp/443->192.168.1.50:",
"hosts-2222 tcp/2222->192.168.1.60:",
" udp/5000->192.168.1.70:5001",
" udp/6000->192.168.1.71:",
}
if got := strings.Join(fwds, "\n"); got != strings.Join(want, "\n") {
t.Errorf("forwards:\n%s\nwant:\n%s", got, strings.Join(want, "\n"))
}
// In order; the jump in the verdict map is inlined.
want = []string{
"input in= from= accept",
"input in=lan from= tcp/22 accept",
"input in=lan from= tcp/23 drop",
"input in=lan from=192.168.1.0/24 tcp/80 accept",
"input in=wan from= udp/53 accept",
"input in=wan from= udp/123 drop",
"input in= from=10.0.0.0/8 accept",
"input in= from=192.0.2.1 drop",
}
if got := strings.Join(rules, "\n"); got != strings.Join(want, "\n") {
t.Errorf("rules:\n%s\nwant:\n%s", got, strings.Join(want, "\n"))
}
notes := strings.Join(res.Notes, "\n")
for _, n := range []string{"map portforwards: its 2 elements became 2 port forwards", "map hosts: its element became a port forward",
"uses the map @pairs, which couldn't be expanded"} {
if !strings.Contains(notes, n) {
t.Errorf("note %q missing:\n%s", n, notes)
}
}
if len(res.Raw) > 0 {
t.Errorf("raw rules: %v", res.Raw)
}
if ps := validate.Errors(validate.Check(res.State, nil)); len(ps) > 0 {
t.Errorf("invalid: %+v", ps)
}
if cmd := strings.Fields(os.Getenv("HOUSECARL_NFT")); len(cmd) > 0 {
c := exec.Command(cmd[0], append(cmd[1:], "-c", "-f", "/dev/stdin")...)
c.Stdin = strings.NewReader(render.Render(res.State, render.Options{}).Text)
if out, err := c.CombinedOutput(); err != nil {
t.Errorf("nft -c: %v\n%s", err, out)
}
}
}
+1
View File
@@ -0,0 +1 @@
{"nftables": [{"metainfo": {"version": "1.1.3", "release_name": "Commodore Bullmoose #4", "json_schema_version": 1}}, {"table": {"family": "inet", "name": "main", "handle": 3}}, {"chain": {"family": "inet", "table": "main", "name": "web", "handle": 1}}, {"chain": {"family": "inet", "table": "main", "name": "prerouting", "handle": 2, "type": "nat", "hook": "prerouting", "prio": -100, "policy": "accept"}}, {"chain": {"family": "inet", "table": "main", "name": "input", "handle": 3, "type": "filter", "hook": "input", "prio": 0, "policy": "drop"}}, {"map": {"family": "inet", "name": "portforwards", "table": "main", "type": "inet_service", "handle": 4, "map": ["ipv4_addr", "inet_service"], "elem": [[80, {"concat": ["192.168.1.50", 8080]}], [443, {"concat": ["192.168.1.50", 443]}]]}}, {"map": {"family": "inet", "name": "hosts", "table": "main", "type": "inet_service", "handle": 5, "map": "ipv4_addr", "elem": [[2222, "192.168.1.60"]]}}, {"map": {"family": "inet", "name": "lan_services", "table": "main", "type": "inet_service", "handle": 6, "map": "verdict", "elem": [[22, {"accept": null}], [23, {"drop": null}], [80, {"jump": {"target": "web"}}]]}}, {"map": {"family": "inet", "name": "pairs", "table": "main", "type": ["ipv4_addr", "inet_service"], "handle": 7, "map": "verdict", "elem": [[{"concat": ["192.168.1.9", 22]}, {"accept": null}]]}}, {"rule": {"family": "inet", "table": "main", "chain": "web", "handle": 8, "expr": [{"match": {"op": "==", "left": {"payload": {"protocol": "ip", "field": "saddr"}}, "right": {"prefix": {"addr": "192.168.1.0", "len": 24}}}}, {"accept": null}]}}, {"rule": {"family": "inet", "table": "main", "chain": "prerouting", "handle": 9, "comment": "Port forwards", "expr": [{"match": {"op": "==", "left": {"meta": {"key": "iifname"}}, "right": "wan"}}, {"dnat": {"family": "ip", "addr": {"map": {"key": {"payload": {"protocol": "tcp", "field": "dport"}}, "data": "@portforwards"}}}}]}}, {"rule": {"family": "inet", "table": "main", "chain": "prerouting", "handle": 10, "expr": [{"match": {"op": "==", "left": {"meta": {"key": "iifname"}}, "right": "wan"}}, {"dnat": {"family": "ip", "addr": {"map": {"key": {"payload": {"protocol": "tcp", "field": "dport"}}, "data": "@hosts"}}}}]}}, {"rule": {"family": "inet", "table": "main", "chain": "prerouting", "handle": 12, "expr": [{"match": {"op": "==", "left": {"meta": {"key": "iifname"}}, "right": "wan"}}, {"dnat": {"family": "ip", "addr": {"map": {"key": {"payload": {"protocol": "udp", "field": "dport"}}, "data": {"set": [[5000, {"concat": ["192.168.1.70", 5001]}], [6000, {"concat": ["192.168.1.71", 6000]}]]}}}}}]}}, {"rule": {"family": "inet", "table": "main", "chain": "input", "handle": 13, "expr": [{"match": {"op": "in", "left": {"ct": {"key": "state"}}, "right": ["established", "related"]}}, {"accept": null}]}}, {"rule": {"family": "inet", "table": "main", "chain": "input", "handle": 14, "expr": [{"match": {"op": "==", "left": {"meta": {"key": "iifname"}}, "right": "lan"}}, {"vmap": {"key": {"payload": {"protocol": "tcp", "field": "dport"}}, "data": "@lan_services"}}]}}, {"rule": {"family": "inet", "table": "main", "chain": "input", "handle": 16, "expr": [{"match": {"op": "==", "left": {"meta": {"key": "iifname"}}, "right": "wan"}}, {"vmap": {"key": {"payload": {"protocol": "udp", "field": "dport"}}, "data": {"set": [[53, {"accept": null}], [123, {"drop": null}]]}}}]}}, {"rule": {"family": "inet", "table": "main", "chain": "input", "handle": 18, "expr": [{"vmap": {"key": {"payload": {"protocol": "ip", "field": "saddr"}}, "data": {"set": [[{"prefix": {"addr": "10.0.0.0", "len": 8}}, {"accept": null}], ["192.0.2.1", {"drop": null}]]}}}]}}, {"rule": {"family": "inet", "table": "main", "chain": "input", "handle": 19, "expr": [{"vmap": {"key": {"concat": [{"payload": {"protocol": "ip", "field": "saddr"}}, {"payload": {"protocol": "tcp", "field": "dport"}}]}, "data": "@pairs"}}]}}]}
+36
View File
@@ -0,0 +1,36 @@
# Maps and verdict maps, as found in hand-written rulesets (importer_test.go).
table inet main {
map portforwards {
type inet_service : ipv4_addr . inet_service
elements = { 443 : 192.168.1.50 . 443, 80 : 192.168.1.50 . 8080 }
}
map hosts {
type inet_service : ipv4_addr
elements = { 2222 : 192.168.1.60 }
}
map lan_services {
type inet_service : verdict
elements = { 22 : accept, 23 : drop, 80 : jump web }
}
map pairs {
type ipv4_addr . inet_service : verdict
elements = { 192.168.1.9 . 22 : accept }
}
chain web {
ip saddr 192.168.1.0/24 accept
}
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
iifname "wan" dnat ip to tcp dport map @portforwards comment "Port forwards"
iifname "wan" dnat ip to tcp dport map @hosts
iifname "wan" dnat ip to udp dport map { 5000 : 192.168.1.70 . 5001, 6000 : 192.168.1.71 . 6000 }
}
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
iifname "lan" tcp dport vmap @lan_services
iifname "wan" udp dport vmap { 53 : accept, 123 : drop }
ip saddr vmap { 10.0.0.0/8 : accept, 192.0.2.1 : drop }
ip saddr . tcp dport vmap @pairs
}
}
+44
View File
@@ -0,0 +1,44 @@
table inet main { # handle 3
map portforwards { # handle 4
type inet_service : ipv4_addr . inet_service
elements = { 80 : 192.168.1.50 . 8080,
443 : 192.168.1.50 . 443 }
}
map hosts { # handle 5
type inet_service : ipv4_addr
elements = { 2222 : 192.168.1.60 }
}
map lan_services { # handle 6
type inet_service : verdict
elements = { 22 : accept,
23 : drop,
80 : jump web }
}
map pairs { # handle 7
type ipv4_addr . inet_service : verdict
elements = { 192.168.1.9 . 22 : accept }
}
chain web { # handle 1
ip saddr 192.168.1.0/24 accept # handle 8
}
chain prerouting { # handle 2
type nat hook prerouting priority dstnat; policy accept;
iifname "wan" dnat ip to tcp dport map @portforwards comment "Port forwards" # handle 9
iifname "wan" dnat ip to tcp dport map @hosts # handle 10
iifname "wan" dnat ip to udp dport map { 5000 : 192.168.1.70 . 5001, 6000 : 192.168.1.71 . 6000 } # handle 12
}
chain input { # handle 3
type filter hook input priority filter; policy drop;
ct state established,related accept # handle 13
iifname "lan" tcp dport vmap @lan_services # handle 14
iifname "wan" udp dport vmap { 53 : accept, 123 : drop } # handle 16
ip saddr vmap { 10.0.0.0/8 : accept, 192.0.2.1 : drop } # handle 18
ip saddr . tcp dport vmap @pairs # handle 19
}
}