Add changesets, bandwidth limits and quotas; generic examples
Changesets, as in Jarl: configuration changes are operations that can be staged in a changeset, checked as they are added (validation and nft -c), reviewed with the diff of the ruleset, and applied together as one history entry, then on probation like any change made over the network. The web UI collects edits for review by default (pending rows, a bar with Review and Apply now, a Changes page, a switch for live edits); hc stages with -C ID or HOUSECARL_CHANGESET (checked like JARL_CHANGESET) and has hc changeset create/list/show/diff/apply/discard/remove. Undo, import and takeover refuse a changeset. --dry-run shows the ruleset's diff. Bandwidth and quotas: rate limits in bytes, per source or destination; quotas per rule or per address that reset daily, weekly, monthly on a day, or by hand, with their usage carried across reloads, saved for reboots, shown by hc quotas and on the rules page, and reset with hc rule reset-quota. Rules that count traffic are checked before established connections are accepted (so they see every packet) and can't accept. Per-address limits group IPv6 by /64 by default (--group-v4/-v6). Per-source rate meters use update. The importer turns meters, byte rates and quotas into these fields. The examples in the web UI, hc's help and the docs use documentation addresses and generic names instead of the maintainer's network. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ebec2cfcca
commit
2880f7ddc9
63 files changed
+4018
-385
No files matched your search
+237
-27
@@ -13,6 +13,7 @@ package importer
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -115,6 +116,7 @@ type importer struct {
|
||||
portNames map[string]string // canonical port list → define name
|
||||
ifNames map[string]string // interface → define name
|
||||
used map[string]bool // sets created
|
||||
meters map[string]bool // sets used as meters
|
||||
noted map[string]bool
|
||||
positions map[string]float64
|
||||
}
|
||||
@@ -199,6 +201,7 @@ func Import(in Input) (*Result, error) {
|
||||
}
|
||||
im.readText()
|
||||
im.readDefines()
|
||||
im.findMeters()
|
||||
im.importSets()
|
||||
im.run()
|
||||
im.nameForwards()
|
||||
@@ -313,6 +316,27 @@ func validIface(s string) bool {
|
||||
// ---- sets
|
||||
|
||||
// importSets turns the table's named address sets into sets.
|
||||
// findMeters notes the sets rules use as meters (set statements: "update
|
||||
// @m { ip saddr limit ... }"): they hold per-address state, not
|
||||
// addresses, and become the rules' per-address limits and quotas.
|
||||
func (im *importer) findMeters() {
|
||||
im.meters = map[string]bool{}
|
||||
for _, rules := range im.rules {
|
||||
for _, r := range rules {
|
||||
for _, e := range r.Expr {
|
||||
var x struct {
|
||||
Set *struct {
|
||||
Set string `json:"set"`
|
||||
} `json:"set"`
|
||||
}
|
||||
if json.Unmarshal(e, &x) == nil && x.Set != nil {
|
||||
im.meters[strings.TrimPrefix(x.Set.Set, "@")] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (im *importer) importSets() {
|
||||
names := make([]string, 0, len(im.sets))
|
||||
for n := range im.sets {
|
||||
@@ -320,6 +344,9 @@ func (im *importer) importSets() {
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, n := range names {
|
||||
if im.meters[n] {
|
||||
continue
|
||||
}
|
||||
s := im.sets[n]
|
||||
var typ string
|
||||
json.Unmarshal(s.Type, &typ)
|
||||
@@ -456,19 +483,24 @@ func values(raw json.RawMessage) []string {
|
||||
|
||||
// parsed is one rule, read.
|
||||
type parsed struct {
|
||||
inIf, outIf []string
|
||||
notIn, notOut bool
|
||||
src, dst []string // entries, or "@set"
|
||||
notSrc, notDst bool
|
||||
family string
|
||||
proto string
|
||||
dports []string
|
||||
icmp []string
|
||||
icmpProto string
|
||||
ctState, ctStatus []string
|
||||
hop255 bool
|
||||
tcpSyn bool
|
||||
limit *model.Limit
|
||||
inIf, outIf []string
|
||||
notIn, notOut bool
|
||||
src, dst []string // entries, or "@set"
|
||||
notSrc, notDst bool
|
||||
family string
|
||||
proto string
|
||||
dports []string
|
||||
icmp []string
|
||||
icmpProto string
|
||||
ctState, ctStatus []string
|
||||
hop255 bool
|
||||
tcpSyn bool
|
||||
limit *model.Limit
|
||||
quota *model.Quota
|
||||
connLimit int
|
||||
// group4, group6: a meter's key grouped by a prefix (ip saddr and
|
||||
// 255.255.255.0); 0 for none.
|
||||
group4, group6 int
|
||||
log bool
|
||||
logPrefix string
|
||||
verdict string // accept, drop, reject, jump, goto, return
|
||||
@@ -547,19 +579,21 @@ func (p *parsed) expr(k string, v json.RawMessage) {
|
||||
json.Unmarshal(v, &x)
|
||||
p.log, p.logPrefix = true, x.Prefix
|
||||
case "limit":
|
||||
var x struct {
|
||||
Rate int `json:"rate"`
|
||||
Per string `json:"per"`
|
||||
Burst int `json:"burst"`
|
||||
Inv bool `json:"inv"`
|
||||
Unit string `json:"rate_unit"`
|
||||
}
|
||||
json.Unmarshal(v, &x)
|
||||
if x.Unit != "" && x.Unit != "packets" {
|
||||
p.bad("a byte rate limit")
|
||||
l, ok := limitJSON(v)
|
||||
if !ok {
|
||||
p.bad("a limit Housecarl can't read")
|
||||
return
|
||||
}
|
||||
p.limit = &model.Limit{Rate: x.Rate, Per: x.Per, Burst: x.Burst, Over: x.Inv}
|
||||
p.limit = l
|
||||
case "quota":
|
||||
q, ok := quotaJSON(v)
|
||||
if !ok {
|
||||
p.bad("a quota Housecarl can't read")
|
||||
return
|
||||
}
|
||||
p.quota = q
|
||||
case "set":
|
||||
p.meter(v)
|
||||
case "dnat", "snat":
|
||||
var x struct {
|
||||
Family string `json:"family"`
|
||||
@@ -625,6 +659,172 @@ func (p *parsed) expr(k string, v json.RawMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
// limitJSON reads a limit: packets, or bytes (rate_unit, burst_unit).
|
||||
func limitJSON(v json.RawMessage) (*model.Limit, bool) {
|
||||
var x struct {
|
||||
Rate int64 `json:"rate"`
|
||||
Per string `json:"per"`
|
||||
Burst int64 `json:"burst"`
|
||||
Inv bool `json:"inv"`
|
||||
Unit string `json:"rate_unit"`
|
||||
BurstUnit string `json:"burst_unit"`
|
||||
}
|
||||
if json.Unmarshal(v, &x) != nil {
|
||||
return nil, false
|
||||
}
|
||||
l := &model.Limit{Rate: int(x.Rate), Per: x.Per, Burst: int(x.Burst), Over: x.Inv}
|
||||
if x.Unit == "" || x.Unit == "packets" {
|
||||
return l, true
|
||||
}
|
||||
// Bytes: both in the largest unit that holds them exactly.
|
||||
rm, ok1 := model.ByteUnits[x.Unit]
|
||||
bu := x.BurstUnit
|
||||
if bu == "" {
|
||||
bu = "bytes"
|
||||
}
|
||||
bm, ok2 := model.ByteUnits[bu]
|
||||
if !ok1 || !ok2 {
|
||||
return nil, false
|
||||
}
|
||||
rate, burst := x.Rate*rm, x.Burst*bm
|
||||
for _, u := range []string{"mbytes", "kbytes", "bytes"} {
|
||||
m := model.ByteUnits[u]
|
||||
if rate%m == 0 && burst%m == 0 {
|
||||
l.Rate, l.Burst, l.Unit = int(rate/m), int(burst/m), u
|
||||
break
|
||||
}
|
||||
}
|
||||
return l, true
|
||||
}
|
||||
|
||||
// quotaJSON reads a quota: {"val": 100, "val_unit": "mbytes", "inv": true}.
|
||||
func quotaJSON(v json.RawMessage) (*model.Quota, bool) {
|
||||
var x struct {
|
||||
Val int64 `json:"val"`
|
||||
Unit string `json:"val_unit"`
|
||||
Inv bool `json:"inv"`
|
||||
}
|
||||
if json.Unmarshal(v, &x) != nil {
|
||||
return nil, false
|
||||
}
|
||||
m, ok := model.ByteUnits[x.Unit]
|
||||
if x.Unit == "" {
|
||||
m, ok = 1, true
|
||||
}
|
||||
if !ok || x.Val < 1 {
|
||||
return nil, false
|
||||
}
|
||||
return &model.Quota{Bytes: x.Val * m, Over: x.Inv}, true
|
||||
}
|
||||
|
||||
// meter reads a set statement that keeps a limit, connection count or
|
||||
// quota per address (a meter: "update @m { ip saddr limit rate over
|
||||
// 3/minute }"), keyed by source or destination, perhaps grouped by a
|
||||
// prefix.
|
||||
func (p *parsed) meter(v json.RawMessage) {
|
||||
var x struct {
|
||||
Op string `json:"op"`
|
||||
Elem json.RawMessage `json:"elem"`
|
||||
Stmt []json.RawMessage `json:"stmt"`
|
||||
}
|
||||
if json.Unmarshal(v, &x) != nil || (x.Op != "add" && x.Op != "update") || len(x.Stmt) != 1 {
|
||||
p.bad("a set statement")
|
||||
return
|
||||
}
|
||||
// The key: ip saddr, or ip saddr and MASK.
|
||||
var key map[string]json.RawMessage
|
||||
json.Unmarshal(x.Elem, &key)
|
||||
bits := 0
|
||||
if and, ok := key["&"]; ok {
|
||||
var pair []json.RawMessage
|
||||
var mask string
|
||||
if json.Unmarshal(and, &pair) != nil || len(pair) != 2 || json.Unmarshal(pair[1], &mask) != nil {
|
||||
p.bad("a meter key")
|
||||
return
|
||||
}
|
||||
a, err := netip.ParseAddr(mask)
|
||||
if err != nil {
|
||||
p.bad("a meter key")
|
||||
return
|
||||
}
|
||||
pfx, ok := maskBits(a)
|
||||
if !ok {
|
||||
p.bad("a meter key with a mask that isn't a prefix")
|
||||
return
|
||||
}
|
||||
bits = pfx
|
||||
key = nil
|
||||
json.Unmarshal(pair[0], &key)
|
||||
}
|
||||
var pl struct{ Protocol, Field string }
|
||||
if raw, ok := key["payload"]; !ok || json.Unmarshal(raw, &pl) != nil || (pl.Protocol != "ip" && pl.Protocol != "ip6") || (pl.Field != "saddr" && pl.Field != "daddr") {
|
||||
p.bad("a meter keyed by something other than an address")
|
||||
return
|
||||
}
|
||||
if bits > 0 {
|
||||
if pl.Protocol == "ip" {
|
||||
p.group4 = bits
|
||||
} else {
|
||||
p.group6 = bits
|
||||
}
|
||||
}
|
||||
if p.family == "" {
|
||||
p.family = map[string]string{"ip": "ipv4", "ip6": "ipv6"}[pl.Protocol]
|
||||
}
|
||||
var stmt map[string]json.RawMessage
|
||||
json.Unmarshal(x.Stmt[0], &stmt)
|
||||
switch {
|
||||
case stmt["limit"] != nil:
|
||||
l, ok := limitJSON(stmt["limit"])
|
||||
if !ok {
|
||||
p.bad("a meter's limit")
|
||||
return
|
||||
}
|
||||
l.PerSource, l.PerDestination = pl.Field == "saddr", pl.Field == "daddr"
|
||||
p.limit = l
|
||||
case stmt["ct count"] != nil:
|
||||
var c struct {
|
||||
Val int `json:"val"`
|
||||
Inv bool `json:"inv"`
|
||||
}
|
||||
if json.Unmarshal(stmt["ct count"], &c) != nil || !c.Inv || pl.Field != "saddr" {
|
||||
p.bad("a connection count other than over N per source")
|
||||
return
|
||||
}
|
||||
p.connLimit = c.Val
|
||||
case stmt["quota"] != nil:
|
||||
q, ok := quotaJSON(stmt["quota"])
|
||||
if !ok {
|
||||
p.bad("a meter's quota")
|
||||
return
|
||||
}
|
||||
q.PerSource, q.PerDestination = pl.Field == "saddr", pl.Field == "daddr"
|
||||
p.quota = q
|
||||
default:
|
||||
p.bad("a set statement")
|
||||
}
|
||||
}
|
||||
|
||||
// maskBits is a mask's prefix length (255.255.255.0: 24), if it is one:
|
||||
// ones, then only zeros.
|
||||
func maskBits(a netip.Addr) (int, bool) {
|
||||
n, zero := 0, false
|
||||
for _, x := range a.AsSlice() {
|
||||
for bit := 7; bit >= 0; bit-- {
|
||||
one := x&(1<<bit) != 0
|
||||
switch {
|
||||
case one && zero:
|
||||
return 0, false
|
||||
case one:
|
||||
n++
|
||||
default:
|
||||
zero = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return n, true
|
||||
}
|
||||
|
||||
func (p *parsed) match(m match) {
|
||||
var left map[string]json.RawMessage
|
||||
json.Unmarshal(m.Left, &left)
|
||||
@@ -972,7 +1172,7 @@ var essentials6 = map[string]bool{"destination-unreachable": true, "packet-too-b
|
||||
var nd6 = map[string]bool{"nd-router-solicit": true, "nd-router-advert": true, "nd-neighbor-solicit": true, "nd-neighbor-advert": true}
|
||||
|
||||
func plain(p *parsed) bool {
|
||||
return len(p.unsupported) == 0 && p.limit == nil && !p.log && len(p.src) == 0 && len(p.dst) == 0 && len(p.outIf) == 0
|
||||
return len(p.unsupported) == 0 && p.limit == nil && p.quota == nil && p.connLimit == 0 && !p.log && len(p.src) == 0 && len(p.dst) == 0 && len(p.outIf) == 0
|
||||
}
|
||||
|
||||
// builtinOf names the built-in a rule is, or "".
|
||||
@@ -1203,7 +1403,8 @@ func (im *importer) rule(chain string, p *parsed) {
|
||||
return
|
||||
}
|
||||
r := &model.Rule{ID: im.newID(), Chain: chain, Position: im.nextPos(chain), Comment: p.comment, CtState: p.ctState, CtStatus: p.ctStatus,
|
||||
Limit: p.limit, Log: p.log, LogPrefix: p.logPrefix, RejectWith: p.rejectWith}
|
||||
Limit: p.limit, Quota: p.quota, ConnLimit: p.connLimit, GroupV4: p.group4, GroupV6: p.group6,
|
||||
Log: p.log, LogPrefix: p.logPrefix, RejectWith: p.rejectWith}
|
||||
switch p.verdict {
|
||||
case "accept", "drop", "reject":
|
||||
r.Action = p.verdict
|
||||
@@ -1222,6 +1423,15 @@ func (im *importer) rule(chain string, p *parsed) {
|
||||
p.bad(p.verdict)
|
||||
}
|
||||
m, ok := im.toMatch(p)
|
||||
if r.CountsTraffic() && r.Action == model.ActionAccept {
|
||||
// Housecarl checks these before established connections are
|
||||
// accepted, where accepting would skip the later rules: as written.
|
||||
p.bad("an accept with a quota or a byte rate")
|
||||
ok = false
|
||||
}
|
||||
if ok && r.CountsTraffic() && chain != model.ChainEarly {
|
||||
im.note("%s rule handle %d has a quota or a byte rate: Housecarl checks it before established connections are accepted, so it counts every packet of a connection", chain, p.handle)
|
||||
}
|
||||
if ok && !p.hop255 && !p.mss && !p.tcpSyn && !p.flowAdd && p.dnatAddr == "" && !p.masq {
|
||||
r.Match = m
|
||||
im.st.Rules = append(im.st.Rules, r)
|
||||
@@ -1315,7 +1525,7 @@ func forwardName(comment string) string {
|
||||
}
|
||||
|
||||
// nameForwards makes the forwards' names (from their comments) valid and
|
||||
// unique: "8096 to jellyfin" becomes "fwd-8096-to-jellyfin", a second
|
||||
// unique: "8080 to web" becomes "fwd-8080-to-web", a second
|
||||
// "web" becomes "web-2".
|
||||
func (im *importer) nameForwards() {
|
||||
seen := map[string]bool{}
|
||||
|
||||
@@ -239,3 +239,65 @@ func TestImportMaps(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestImportMeters: meters (per-address limits, connection counts and
|
||||
// quotas), byte rates and quotas become rules' limits and quotas
|
||||
// (testdata/meters.nft); the meters' sets aren't imported as address sets.
|
||||
func TestImportMeters(t *testing.T) {
|
||||
js, _ := os.ReadFile("testdata/meters.json")
|
||||
txt, _ := os.ReadFile("testdata/meters.txt")
|
||||
res, err := Import(Input{JSON: js, Text: string(txt)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got []string
|
||||
for _, r := range res.State.Rules {
|
||||
s := r.Chain
|
||||
if l := r.Limit; l != nil {
|
||||
s += fmt.Sprintf(" limit %d %s/%s burst %d over=%v src=%v dst=%v", l.Rate, l.Unit, l.Per, l.Burst, l.Over, l.PerSource, l.PerDestination)
|
||||
}
|
||||
if r.ConnLimit > 0 {
|
||||
s += fmt.Sprintf(" conn %d", r.ConnLimit)
|
||||
}
|
||||
if q := r.Quota; q != nil {
|
||||
s += fmt.Sprintf(" quota %d over=%v src=%v", q.Bytes, q.Over, q.PerSource)
|
||||
}
|
||||
if r.GroupV4 != 0 || r.GroupV6 != 0 {
|
||||
s += fmt.Sprintf(" group %d/%d", r.GroupV4, r.GroupV6)
|
||||
}
|
||||
s += " " + r.Action + r.Raw
|
||||
got = append(got, s)
|
||||
}
|
||||
want := []string{
|
||||
"input accept", // established: only in input here, so a rule
|
||||
"input limit 3 /minute burst 5 over=true src=true dst=false drop",
|
||||
"input limit 3 /minute burst 5 over=true src=true dst=false group 0/64 drop",
|
||||
"input accept",
|
||||
"forward conn 20 drop",
|
||||
"forward limit 10240 kbytes/second burst 512 over=true src=false dst=false drop",
|
||||
"forward quota 1048576000 over=true src=true group 24/0 drop",
|
||||
"forward quota 104857600000 over=true src=false drop",
|
||||
"forward accept",
|
||||
}
|
||||
if strings.Join(got, "\n") != strings.Join(want, "\n") {
|
||||
t.Errorf("rules:\n%s\nwant:\n%s\nnotes: %v", strings.Join(got, "\n"), strings.Join(want, "\n"), res.Notes)
|
||||
}
|
||||
for _, s := range res.State.Sets {
|
||||
if s.Name != "trusted" {
|
||||
t.Errorf("a meter became a set: %s", s.Name)
|
||||
}
|
||||
}
|
||||
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
@@ -0,0 +1 @@
|
||||
{"nftables": [{"metainfo": {"version": "1.1.3", "release_name": "Commodore Bullmoose #4", "json_schema_version": 1}}, {"table": {"family": "inet", "name": "main", "handle": 1}}, {"chain": {"family": "inet", "table": "main", "name": "input", "handle": 1, "type": "filter", "hook": "input", "prio": 0, "policy": "drop"}}, {"chain": {"family": "inet", "table": "main", "name": "forward", "handle": 2, "type": "filter", "hook": "forward", "prio": 0, "policy": "drop"}}, {"chain": {"family": "inet", "table": "main", "name": "output", "handle": 3, "type": "filter", "hook": "output", "prio": 0, "policy": "accept"}}, {"set": {"family": "inet", "name": "ssh_meter", "table": "main", "type": "ipv4_addr", "handle": 4, "size": 65535, "flags": ["timeout", "dynamic"], "timeout": 60}}, {"set": {"family": "inet", "name": "ssh6_meter", "table": "main", "type": "ipv6_addr", "handle": 5, "size": 65535, "flags": ["timeout", "dynamic"], "timeout": 60}}, {"set": {"family": "inet", "name": "my_connlimit", "table": "main", "type": "ipv4_addr", "handle": 6, "size": 65535, "flags": ["dynamic"]}}, {"set": {"family": "inet", "name": "guest_quota", "table": "main", "type": "ipv4_addr", "handle": 7, "size": 65535, "flags": ["dynamic"]}}, {"set": {"family": "inet", "name": "trusted", "table": "main", "type": "ipv4_addr", "handle": 8, "flags": ["interval"], "elem": [{"prefix": {"addr": "10.0.10.0", "len": 24}}]}}, {"rule": {"family": "inet", "table": "main", "chain": "input", "handle": 9, "expr": [{"match": {"op": "in", "left": {"ct": {"key": "state"}}, "right": ["established", "related"]}}, {"accept": null}]}}, {"rule": {"family": "inet", "table": "main", "chain": "input", "handle": 10, "expr": [{"match": {"op": "==", "left": {"payload": {"protocol": "tcp", "field": "dport"}}, "right": 22}}, {"match": {"op": "in", "left": {"ct": {"key": "state"}}, "right": "new"}}, {"set": {"op": "update", "elem": {"payload": {"protocol": "ip", "field": "saddr"}}, "set": "@ssh_meter", "stmt": [{"limit": {"rate": 3, "burst": 5, "per": "minute", "inv": true}}]}}, {"counter": {"packets": 0, "bytes": 0}}, {"drop": null}]}}, {"rule": {"family": "inet", "table": "main", "chain": "input", "handle": 11, "expr": [{"match": {"op": "==", "left": {"payload": {"protocol": "tcp", "field": "dport"}}, "right": 22}}, {"match": {"op": "in", "left": {"ct": {"key": "state"}}, "right": "new"}}, {"set": {"op": "update", "elem": {"&": [{"payload": {"protocol": "ip6", "field": "saddr"}}, "ffff:ffff:ffff:ffff::"]}, "set": "@ssh6_meter", "stmt": [{"limit": {"rate": 3, "burst": 5, "per": "minute", "inv": true}}]}}, {"counter": {"packets": 0, "bytes": 0}}, {"drop": null}]}}, {"rule": {"family": "inet", "table": "main", "chain": "input", "handle": 12, "expr": [{"match": {"op": "==", "left": {"payload": {"protocol": "ip", "field": "saddr"}}, "right": "@trusted"}}, {"match": {"op": "==", "left": {"payload": {"protocol": "tcp", "field": "dport"}}, "right": 22}}, {"accept": null}]}}, {"rule": {"family": "inet", "table": "main", "chain": "forward", "handle": 13, "expr": [{"match": {"op": "in", "left": {"ct": {"key": "state"}}, "right": "new"}}, {"set": {"op": "add", "elem": {"payload": {"protocol": "ip", "field": "saddr"}}, "set": "@my_connlimit", "stmt": [{"ct count": {"val": 20, "inv": true}}]}}, {"counter": {"packets": 0, "bytes": 0}}, {"drop": null}]}}, {"rule": {"family": "inet", "table": "main", "chain": "forward", "handle": 14, "expr": [{"match": {"op": "==", "left": {"meta": {"key": "iifname"}}, "right": "lan"}}, {"match": {"op": "==", "left": {"meta": {"key": "oifname"}}, "right": "wan"}}, {"limit": {"rate": 10, "burst": 512, "per": "second", "inv": true, "rate_unit": "mbytes", "burst_unit": "kbytes"}}, {"drop": null}]}}, {"rule": {"family": "inet", "table": "main", "chain": "forward", "handle": 15, "expr": [{"match": {"op": "==", "left": {"meta": {"key": "iifname"}}, "right": "guest0"}}, {"match": {"op": "==", "left": {"meta": {"key": "oifname"}}, "right": "wan"}}, {"set": {"op": "add", "elem": {"&": [{"payload": {"protocol": "ip", "field": "saddr"}}, "255.255.255.0"]}, "set": "@guest_quota", "stmt": [{"quota": {"val": 1000, "val_unit": "mbytes", "inv": true}}]}}, {"drop": null}]}}, {"rule": {"family": "inet", "table": "main", "chain": "forward", "handle": 16, "expr": [{"match": {"op": "==", "left": {"meta": {"key": "iifname"}}, "right": "lan"}}, {"match": {"op": "==", "left": {"meta": {"key": "oifname"}}, "right": "wan"}}, {"quota": {"val": 100000, "val_unit": "mbytes", "inv": true}}, {"drop": null}]}}, {"rule": {"family": "inet", "table": "main", "chain": "forward", "handle": 17, "expr": [{"match": {"op": "==", "left": {"meta": {"key": "iifname"}}, "right": "lan"}}, {"match": {"op": "==", "left": {"meta": {"key": "oifname"}}, "right": "wan"}}, {"accept": null}]}}]}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
# Meters, byte rates and quotas, as hand-written rulesets use them (importer_test.go).
|
||||
table inet main {
|
||||
set ssh_meter {
|
||||
type ipv4_addr
|
||||
flags dynamic,timeout
|
||||
timeout 1m
|
||||
}
|
||||
set ssh6_meter {
|
||||
type ipv6_addr
|
||||
flags dynamic,timeout
|
||||
timeout 1m
|
||||
}
|
||||
set my_connlimit {
|
||||
type ipv4_addr
|
||||
size 65535
|
||||
flags dynamic
|
||||
}
|
||||
set guest_quota {
|
||||
type ipv4_addr
|
||||
size 65535
|
||||
flags dynamic
|
||||
}
|
||||
set trusted {
|
||||
type ipv4_addr
|
||||
flags interval
|
||||
elements = { 10.0.10.0/24 }
|
||||
}
|
||||
chain input {
|
||||
type filter hook input priority filter; policy drop;
|
||||
ct state established,related accept
|
||||
tcp dport 22 ct state new update @ssh_meter { ip saddr limit rate over 3/minute burst 5 packets } counter drop
|
||||
tcp dport 22 ct state new update @ssh6_meter { ip6 saddr and ffff:ffff:ffff:ffff:: limit rate over 3/minute } counter drop
|
||||
ip saddr @trusted tcp dport 22 accept
|
||||
}
|
||||
chain forward {
|
||||
type filter hook forward priority filter; policy drop;
|
||||
ct state new add @my_connlimit { ip saddr ct count over 20 } counter drop
|
||||
iifname "lan" oifname "wan" limit rate over 10 mbytes/second burst 512 kbytes drop
|
||||
iifname "guest0" oifname "wan" add @guest_quota { ip saddr and 255.255.255.0 quota over 1000 mbytes } drop
|
||||
iifname "lan" oifname "wan" quota over 100000 mbytes drop
|
||||
iifname "lan" oifname "wan" accept
|
||||
}
|
||||
chain output {
|
||||
type filter hook output priority filter; policy accept;
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
table inet main { # handle 1
|
||||
set ssh_meter { # handle 4
|
||||
type ipv4_addr
|
||||
size 65535
|
||||
flags dynamic,timeout
|
||||
timeout 1m
|
||||
}
|
||||
|
||||
set ssh6_meter { # handle 5
|
||||
type ipv6_addr
|
||||
size 65535
|
||||
flags dynamic,timeout
|
||||
timeout 1m
|
||||
}
|
||||
|
||||
set my_connlimit { # handle 6
|
||||
type ipv4_addr
|
||||
size 65535
|
||||
flags dynamic
|
||||
}
|
||||
|
||||
set guest_quota { # handle 7
|
||||
type ipv4_addr
|
||||
size 65535
|
||||
flags dynamic
|
||||
}
|
||||
|
||||
set trusted { # handle 8
|
||||
type ipv4_addr
|
||||
flags interval
|
||||
elements = { 10.0.10.0/24 }
|
||||
}
|
||||
|
||||
chain input { # handle 1
|
||||
type filter hook input priority filter; policy drop;
|
||||
ct state established,related accept # handle 9
|
||||
tcp dport 22 ct state new update @ssh_meter { ip saddr limit rate over 3/minute burst 5 packets } counter packets 0 bytes 0 drop # handle 10
|
||||
tcp dport 22 ct state new update @ssh6_meter { ip6 saddr & ffff:ffff:ffff:ffff:: limit rate over 3/minute burst 5 packets } counter packets 0 bytes 0 drop # handle 11
|
||||
ip saddr @trusted tcp dport 22 accept # handle 12
|
||||
}
|
||||
|
||||
chain forward { # handle 2
|
||||
type filter hook forward priority filter; policy drop;
|
||||
ct state new add @my_connlimit { ip saddr ct count over 20 } counter packets 0 bytes 0 drop # handle 13
|
||||
iifname "lan" oifname "wan" limit rate over 10 mbytes/second burst 512 kbytes drop # handle 14
|
||||
iifname "guest0" oifname "wan" add @guest_quota { ip saddr & 255.255.255.0 quota over 1000 mbytes } drop # handle 15
|
||||
iifname "lan" oifname "wan" quota over 100000 mbytes drop # handle 16
|
||||
iifname "lan" oifname "wan" accept # handle 17
|
||||
}
|
||||
|
||||
chain output { # handle 3
|
||||
type filter hook output priority filter; policy accept;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user