grammar doc updates

This commit is contained in:
2026-05-03 17:45:40 -07:00
parent 23ce29aece
commit 30427521ca
2 changed files with 283 additions and 582 deletions

View File

@@ -1,49 +1,27 @@
# FWL Grammar Specification (MVP) # FWL Grammar Specification
## Overview > **Version:** MVP
> **Last updated:** May 2026
FWL is a typed, functional DSL that compiles to nftables JSON. Programs are > This document is the authoritative grammar reference for the Firewall Language (FWL).
sequences of top-level declarations. The grammar uses explicit braces and > It supersedes the syntax examples in `proposal.md` and reflects the current parser implementation.
semicolons throughout — no indentation sensitivity. Types are mandatory on all
top-level declarations for MVP; inference is deferred to a later version.
The target nftables table is a single table named `fwl` by default
(configurable via a top-level `config` declaration). Both filter and NAT
policies compile into this one table.
--- ---
## Notation ## Design Principles
``` - **Explicit delimiters everywhere** — all blocks use `{` `}` with trailing `;` on each item. No layout/indentation sensitivity.
::= production - **Syntactic keywords are reserved** — only words that structurally delimit declarations or expressions are in `reservedNames`. Semantic values (action names, effect labels, constructors) are plain identifiers.
| alternative - **Types are explicit** — top-level declarations carry full type annotations in the MVP.
{ x } zero or more repetitions of x - **Patterns vs. guards are strictly separated** — structural decomposition happens in patterns; boolean predicates over bound names happen in guards.
[ x ] optional x - **IP addresses are integers** — IPv4 is a 32-bit value; IPv6 is a 128-bit value. Named priority constants (`Filter`, `SrcNat`, etc.) lower to their canonical integer values at parse time.
```
String terminals are written in `"double quotes"`. Regex-like character classes
use `[a-z]`, etc.
--- ---
## Top-Level Structure ## Top-Level Program
```ebnf ```ebnf
program ::= { config } { decl } program ::= { decl }
config ::= "config" "{" { configProp ";" } "}"
configProp ::= "table" "=" stringLit
```
Every non-`config` declaration is terminated by `";"`.
---
## Declarations
```ebnf
decl ::= interfaceDecl decl ::= interfaceDecl
| zoneDecl | zoneDecl
| importDecl | importDecl
@@ -54,7 +32,9 @@ decl ::= interfaceDecl
| policyDecl | policyDecl
``` ```
### Interface ---
## Declarations
```ebnf ```ebnf
interfaceDecl ::= "interface" ident ":" ifaceKind "{" { ifaceProp ";" } "}" ";" interfaceDecl ::= "interface" ident ":" ifaceKind "{" { ifaceProp ";" } "}" ";"
@@ -66,429 +46,323 @@ ifaceProp ::= "dynamic"
| "cidr6" "=" cidrSet | "cidr6" "=" cidrSet
cidrSet ::= "{" cidrLit { "," cidrLit } "}" cidrSet ::= "{" cidrLit { "," cidrLit } "}"
```
### Zone
```ebnf
zoneDecl ::= "zone" ident "=" "{" ident { "," ident } "}" ";" zoneDecl ::= "zone" ident "=" "{" ident { "," ident } "}" ";"
```
### Import
```ebnf
importDecl ::= "import" ident ":" type "from" stringLit ";" importDecl ::= "import" ident ":" type "from" stringLit ";"
```
### Let
```ebnf
letDecl ::= "let" ident ":" type "=" expr ";" letDecl ::= "let" ident ":" type "=" expr ";"
```
### Pattern patternDecl ::= "pattern" ident ":" type "=" pat ";"
Named patterns are first-class; they may appear anywhere a structural pattern
appears, including nested inside constructor patterns, `Frame(...)`, and other
named patterns.
```ebnf
patternDecl ::= "pattern" ident ":" type "=" packetPat ";"
```
Named patterns are resolved during type-checking, not by macro-expanding at
parse time. Recursive named patterns are a type error.
### Flow
```ebnf
flowDecl ::= "flow" ident ":" "FlowPattern" "=" flowExpr ";" flowDecl ::= "flow" ident ":" "FlowPattern" "=" flowExpr ";"
flowExpr ::= ident
| ident "." ident "within" duration
flowExpr ::= seqExpr
seqExpr ::= flowAtom
| flowAtom "." seqExpr
| flowAtom "." seqExpr "within" duration
```
A `within` clause applies to the entire sequence to its left and binds most
tightly to the innermost `.`. For MVP, `within` is only valid at the top level
of a `flowExpr`.
```ebnf
flowAtom ::= ident
```
### Rule
Rules are reusable, named packet-processing functions. They receive a `Frame`
and return an `Action` (possibly via effects).
```ebnf
ruleDecl ::= "rule" ident ":" type "=" lambdaExpr ";" ruleDecl ::= "rule" ident ":" type "=" lambdaExpr ";"
lambdaExpr ::= "\" ident "->" expr
```
A `rule` body must be a lambda at the top level for MVP.
### Policy
Policies are the entry points tied to nftables hooks. A policy body is a
bare arm-block (no `case ... of` wrapper; the matched value is always the
bound `Frame`-like parameter of the policy).
```ebnf
policyDecl ::= "policy" ident ":" type policyDecl ::= "policy" ident ":" type
"on" "{" hookSpec "}" "on" "{"
"hook" "=" hook ","
"table" "=" tableName ","
"priority" "=" priority
"}"
"=" armBlock ";" "=" armBlock ";"
```
hookSpec ::= hookProp "," hookProp "," hookProp ### Policy Metadata
| hookProp "," hookProp "," hookProp "," -- trailing comma OK
hookProp ::= "hook" "=" hook
| "table" "=" tableName
| "priority" "=" priority
```ebnf
hook ::= "Input" | "Forward" | "Output" | "Prerouting" | "Postrouting" hook ::= "Input" | "Forward" | "Output" | "Prerouting" | "Postrouting"
tableName ::= "Filter" | "NAT"
priority ::= "Filter" | "DstNat" | "SrcNat" | "Mangle" | intLit tableName ::= "Filter" | "NAT" | ident
-- Priority is always an integer in nftables JSON.
-- Named constants are resolved at parse time:
-- Raw = -300, ConnTrack = -200, Mangle = -150,
-- DstNat = -100, Filter = 0, SrcNat = 100
priority ::= "Filter" | "DstNat" | "SrcNat" | "Mangle"
| "Raw" | "ConnTrack"
| [ "-" ] nat
``` ```
--- ---
## Arm Blocks
Used uniformly inside `rule` bodies (via `case`) and `policy` bodies.
```ebnf
armBlock ::= "{" { arm } "}"
arm ::= "|" pat guardOpt "->" expr ";"
guardOpt ::= ε
| "if" expr
```
---
## Patterns
Patterns describe packet structure and bind names. All membership/comparison
predicates are guards (see § Expressions), not patterns — except for field
constraints inside record patterns, which are written as field predicates.
```ebnf
pat ::= "_" -- wildcard
| ident -- variable binding
| namedPat -- first-class named pattern
| ctorPat -- e.g., IPv4(ip, ...)
| recordPat -- e.g., tcp { dport = :22 }
| tuplePat -- e.g., (udp, payload)
| framePat -- Frame(path, inner)
| bytePat -- e.g., [0x01 _*]
-- A named pattern reference; resolved at type-check time.
-- Binds NO additional names itself (names are bound in the pattern's definition).
namedPat ::= ident -- must refer to a declared pattern
ctorPat ::= ident "(" patList ")"
patList ::= pat { "," pat }
recordPat ::= ident "{" [ fieldPatList ] "}"
fieldPatList::= fieldPat { "," fieldPat }
fieldPat ::= ident "=" literal -- equality constraint
| ident -- bind field to same-named variable
| ident "as" ident -- bind field to fresh variable
tuplePat ::= "(" pat { "," pat } ")"
-- Frame pattern: optional interface-path specifier, then inner packet pattern.
framePat ::= "Frame" "(" [ pathPat "," ] pat ")"
-- Interface-path: source, destination, or both.
pathPat ::= endpointPat
| endpointPat "->" endpointPat
endpointPat ::= "_"
| ident -- exact interface name
| ident "in" ident -- interface is member of zone
```
**Note on `∈`:** the parser accepts both the Unicode `∈` and the ASCII keyword
`in` as synonyms in all positions. The AST stores a single `MemberOf`
constructor.
### Byte Patterns
Used in `pattern` declarations for payload matching.
```ebnf
bytePat ::= "[" { byteElem } "]"
byteElem ::= hexByte -- e.g., 0x01
| "_" -- any single byte
| "_*" -- zero or more bytes
```
---
## Expressions
```ebnf
expr ::= letExpr
| ifExpr
| doExpr
| caseExpr
| infixExpr
letExpr ::= "let" ident "=" expr "in" expr
ifExpr ::= "if" expr "then" expr "else" expr
doExpr ::= "do" "{" doStmt { ";" doStmt } "}"
doStmt ::= ident "<-" expr -- effectful bind
| expr -- effectful sequence
caseExpr ::= "case" expr "of" armBlock
infixExpr ::= prefixExpr { infixOp prefixExpr }
prefixExpr ::= appExpr
| "!" prefixExpr
appExpr ::= atom { atom } -- function application
atom ::= ident
| qualName
| literal
| tupleLit
| setLit
| mapLit
| performExpr
| "(" expr ")"
performExpr ::= "perform" qualName "(" [ argList ] ")"
tupleLit ::= "(" expr "," expr { "," expr } ")"
setLit ::= "{" [ expr { "," expr } ] "}"
mapLit ::= "{" mapEntry { "," mapEntry } "}"
mapEntry ::= expr "->" expr
argList ::= expr { "," expr }
qualName ::= ident { "." ident }
infixOp ::= "&&" | "||"
| "==" | "!=" | "<" | "<=" | ">" | ">="
| "in" | "∈" -- set/zone membership
| "++" -- string/list concat
| ">>" -- effect sequencing
| ">>=" -- monadic bind
```
**Operator precedence** (high to low):
| Level | Operators | Assoc |
|-------|-----------------------|-------|
| 7 | application | left |
| 6 | `==` `!=` `<` `<=` `>` `>=` `in` `∈` | none |
| 5 | `&&` | right |
| 4 | `\|\|` | right |
| 3 | `++` | right |
| 2 | `>>=` | left |
| 1 | `>>` | left |
---
## Types ## Types
```ebnf ```ebnf
type ::= funType type ::= simpleType
| simpleType "->" type -- function type
| "<" effectList ">" type -- effectful function type
funType ::= effectType simpleType ::= ident -- type name (Frame, Action, IP, etc.)
| effectType "->" funType | ident "<" typeList ">" -- generic: Map<K,V>, Bytes<{}>
effectType ::= "<" [ ident { "," ident } ] ">" simpleType
| simpleType
simpleType ::= ident [ "<" typeList ">" ] -- parameterised type
| "(" type { "," type } ")" -- tuple type | "(" type { "," type } ")" -- tuple type
| "(" type ")" -- grouped
typeList ::= type { "," type } typeList ::= type { "," type }
effectList ::= ident { "," ident }
``` ```
Effect rows use angle brackets: `<FlowMatch, Log> Action`. > **Note:** `Frame`, `FlowPattern`, and all action/effect type names (`Action`, `CIDRSet`, etc.)
> are plain identifiers in the type parser — they are **not** reserved keywords.
For MVP, effect annotations are required on `rule` declarations that contain
`perform` expressions and are optional on `policy` declarations.
--- ---
## Actions ## Expressions
`Action` is a built-in type. Its constructors are:
```ebnf ```ebnf
action ::= "Allow" lambdaExpr ::= "\" ident "->" expr
| "Drop" | expr
| "Continue"
| "Masquerade"
| "DNAT" "(" expr ")"
| "DNATMap" "(" expr ")"
| "Log" "(" logLevel "," expr ")"
logLevel ::= "Info" | "Warn" | "Error" expr ::= ifExpr
| doExpr
| infixExpr
ifExpr ::= "if" expr "then" expr "else" expr
doExpr ::= "do" "{" stmt { ";" stmt } "}"
stmt ::= "let" ident "=" expr
| ident "<-" expr
| expr
infixExpr ::= prefixExpr { infixOp prefixExpr }
infixOp ::= "&&" | "||" | "==" | "!=" | "<" | "<=" | ">" | ">="
| "++" | ">>" | ">>=" | "∈" | "in"
prefixExpr ::= "!" prefixExpr | appExpr
appExpr ::= atom { atom }
atom ::= performExpr
| mapLit -- { expr -> expr, ... } tried before setLit
| setLit -- { expr, ... }
| tupleLit -- ( expr, expr, ... ) requires 2
| "(" expr ")"
| literal
| portLit -- :22 :8080
| qualName -- foo foo.bar foo.bar.baz
performExpr ::= "perform" qualName "(" argList? ")"
argList ::= expr { "," expr }
mapLit ::= "{" mapEntry { "," mapEntry } "}"
mapEntry ::= expr "->" expr
setLit ::= "{" expr { "," expr } "}"
tupleLit ::= "(" expr "," expr { "," expr } ")"
qualName ::= ident { "." ident }
``` ```
`Continue` is a legal action value and compiles to nothing (a no-op pass-
through). It is used to make exhaustive arm blocks typecheck when earlier arms
handle all interesting cases. A policy arm that returns `Continue` as the last
arm is a type error (unreachable or missing terminator); a `rule` arm may
return `Continue` to signal "pass control back to the caller."
--- ---
## Effects ## Patterns
The built-in effects available for MVP are: ```ebnf
pat ::= wildcardPat -- _
| framePat -- Frame(...)
| tuplePat -- (p, p, ...) requires 2
| bytesPat -- [ byteElem* ]
| recordPat -- Ctor { field = lit, ... }
| namedOrCtorPat -- Ctor(p,...) or bare identifier
| Effect | Operations | wildcardPat ::= "_"
|------------|---------------------------------------------------| framePat ::= "Frame" "(" frameArgs ")"
| `FlowMatch`| `FlowMatch.check(flowId, pattern) : MatchResult` | frameArgs ::= pathPat "," pat -- with explicit path
| `Log` | `Log.emit(level, msg) : ()` | | pat -- path inferred
| `FIB` | `FIB.daddrLocal(ip) : Bool` |
`MatchResult` constructors: `Matched`, `Unmatched`. pathPat ::= endpointPat? ( "->" endpointPat? )?
endpointPat ::= "_"
| ident "in" ident -- iif in lan_zone
| ident "∈" ident
| ident
Additional effects may be declared by the user in a future version. tuplePat ::= "(" pat "," pat { "," pat } ")"
bytesPat ::= "[" byteElem* "]"
byteElem ::= hexByte -- 0xff
| "_" -- any byte
| "_" "*" -- zero or more bytes
recordPat ::= ident "{" fieldPat { "," fieldPat } "}"
fieldPat ::= ident "=" fieldLit -- exact match
| ident "in" expr -- membership
| ident "∈" expr
| ident "as" ident -- bind with alias
| ident -- bind to same name
-- fieldLit extends literal with port syntax
fieldLit ::= ":" nat | literal
namedOrCtorPat ::= ident "(" pat { "," pat } ")" -- constructor with args
| ident -- variable or nullary ctor
```
---
## Case Arms
```ebnf
armBlock ::= "{" { arm } "}"
arm ::= "|" pat ( "if" expr )? "->" expr ";"
```
--- ---
## Literals ## Literals
```ebnf ```ebnf
literal ::= intLit literal ::= ipOrCidrLit
| stringLit | hexByte -- 0xff
| boolLit
| ipv4Lit
| ipv6Lit
| cidrLit
| portLit
| durationLit
| hexByte
intLit ::= ["-"] digit+
stringLit ::= '"' { strChar } '"'
boolLit ::= "true" | "false"
ipv4Lit ::= octet "." octet "." octet "." octet
ipv6Lit ::= -- standard IPv6 notation including "::" compression
cidrLit ::= (ipv4Lit | ipv6Lit) "/" digit+
portLit ::= ":" digit+ -- e.g., :22, :8080
durationLit ::= digit+ timeUnit
timeUnit ::= "s" | "ms" | "m" | "h"
hexByte ::= "0x" hexDigit hexDigit
octet ::= digit+ -- 0-255
```
---
## Lexical Rules
```ebnf
ident ::= letter { letter | digit | "_" }
-- must not be a reserved word
reserved ::= "config" | "interface" | "zone" | "import" | "let" | "in"
| "pattern" | "flow" | "rule" | "policy" | "on"
| "case" | "of" | "if" | "then" | "else" | "do"
| "perform" | "within" | "as"
| "WAN" | "LAN" | "WireGuard"
| "Input" | "Forward" | "Output" | "Prerouting" | "Postrouting"
| "Filter" | "NAT" | "Mangle" | "DstNat" | "SrcNat"
| "Allow" | "Drop" | "Continue" | "Masquerade" | "DNAT"
| "DNATMap" | "Log" | "Info" | "Warn" | "Error"
| "Matched" | "Unmatched"
| "dynamic" | "cidr4" | "cidr6" | "table" | "hook" | "priority"
| "true" | "false" | "true" | "false"
| "FlowPattern" | "Frame" | stringLit -- "..."
| nat -- decimal integer
comment ::= "--" { any char except newline } portLit ::= ":" nat -- :22, :8080, :51944
| "{-" { any char } "-}"
whitespace ::= space | tab | newline | comment ipOrCidrLit ::= ipLit ( "/" nat )? -- optional prefix → CIDR
ipLit ::= ipv6Lit | ipv4Lit
-- IPv4: four decimal octets 0-255
ipv4Lit ::= octet "." octet "." octet "." octet
octet ::= nat -- 0..255
-- IPv6: full or compressed notation, optional embedded IPv4
-- All standard forms are supported:
-- full: 2001:0db8:85a3:0000:0000:8a2e:0370:7334
-- compressed: 2001:db8::8a2e:370:7334
-- loopback: ::1
-- any: ::
-- link-local: fe80::1
-- IPv4-mapped: ::ffff:192.168.1.1
ipv6Lit ::= ipv6Groups
ipv6Groups ::= "::" ipv6RightGroups? -- starts with ::
| ipv6LeftGroups ( "::" ipv6RightGroups? )?
ipv6LeftGroups ::= hex16 { ":" hex16 } -- stops before ::
ipv6RightGroups ::= ipv4EmbeddedGroups | ipv6LeftGroups
ipv4EmbeddedGroups ::= { hex16 ":" } octet "." octet "." octet "." octet
hex16 ::= hexDigit+ -- 1-4 hex digits, value 0x0000..0xffff
cidrLit ::= ipLit "/" nat -- must be a CIDR (prefix required)
hexByte ::= "0x" hexDigit hexDigit
duration ::= nat timeUnit
timeUnit ::= "s" | "ms" | "m" | "h"
``` ```
Identifiers beginning with an uppercase letter are treated as constructor ### Internal IP Representation
names by convention; those beginning with lowercase are variables. The lexer
does not enforce this — it is a naming convention only, checked during IP addresses are stored as plain `Integer` values, not tuples or byte arrays:
type-checking.
| Type | Storage | Range |
|-------|----------|------------------|
| IPv4 | 32-bit `Integer` | `0x00000000`..`0xFFFFFFFF` |
| IPv6 | 128-bit `Integer` | `0x0`..`0xFFFF...FFFF` |
CIDR host-bit validation: `(addr .&. hostMask) == 0` where `hostMask = (1 << (bits - prefix)) - 1`.
--- ---
## Resolved Inconsistencies from Proposal ## Reserved Keywords
The following decisions were made to normalize the proposal's syntax: Only these words are reserved (i.e. `identifier` will reject them):
| Topic | Proposal state | MVP decision | ```
|-------|---------------|--------------| config table interface zone import from
| Interface body | Multiline, no delimiters | Braced block with `;` separators | let in pattern flow rule policy on
| Policy body | `where` with indented arms | `=` followed by arm-block | case of if then else do perform
| Rule body | `\frame -> case frame of \| ...` | `\ident -> expr`; `case` is a normal expression | within as dynamic cidr4 cidr6
| Policy vs rule | Distinct surface syntax | Policies use a bare arm-block; rules use `case` explicitly | hook priority
| `Frame<{}>` | Unclear `<{}>` parameter | Parsed but ignored for MVP; written as `Frame` in practice | WAN LAN WireGuard
| Named patterns in sub-positions | Unclear | First-class everywhere; resolved at type-check time | Input Forward Output Prerouting Postrouting
| `∈` operator | Unicode only | Both `∈` and `in` accepted everywhere | Filter NAT Mangle DstNat SrcNat
| `Continue` | Unclear semantics | Legal `Action` constructor; compiles to nothing; type error if last arm of a policy block | Raw ConnTrack
| Single nftables table | Not specified | Default table name `fwl`; configurable via `config { table = "name"; }` | true false
| `handle` syntax | Mentioned but unspecified | Deferred; MVP only has `perform` | ```
| Effect annotations | Inconsistent (`<>` vs `{}`) | Angle brackets `<Eff1, Eff2>` everywhere |
| Guard vs pattern membership | Mixed | Structural matching in patterns only; `in`/`∈` in guards only (except `fieldPat`) | The following are **not** reserved and parse as plain identifiers in all positions
(type names, constructors, action values, effect labels):
```
Frame FlowPattern
Allow Drop Continue Masquerade DNAT DNATMap
Log Info Warn Error
Matched Unmatched
Action Packet IP Port Protocol
CIDRSet Map Bytes
```
---
## Priority Constants
Named priorities resolve to integers at parse time:
| Name | Integer value |
|-------------|---------------|
| `Raw` | -300 |
| `ConnTrack` | -200 |
| `Mangle` | -150 |
| `DstNat` | -100 |
| `Filter` | 0 |
| `SrcNat` | 100 |
Arbitrary integers (including negative, e.g. `-150`) are also accepted.
---
## Operator Precedence
From lowest to highest binding:
| Level | Operators | Associativity |
|-------|------------------------|---------------|
| 1 | `if … then … else` | — |
| 2 | `\|\|` | left |
| 3 | `&&` | left |
| 4 | `==` `!=` | none |
| 5 | `<` `<=` `>` `>=` | none |
| 6 | `∈` `in` | none |
| 7 | `++` `>>` `>>=` | left |
| 8 | `!` (prefix) | — |
| 9 | function application | left |
--- ---
## Canonical Examples ## Canonical Examples
The following examples must all parse under the grammar above. ### Interface and zone declarations
### 1. Interface and Zone
```fwl ```fwl
interface wan : WAN { dynamic; }; interface wan : WAN { dynamic; };
interface lan : LAN { interface lan : LAN { cidr4 = { 10.17.1.0/24 }; };
cidr4 = { 10.17.1.0/24 };
cidr6 = { fe80::/10, fd12:3456::/48 };
};
interface wg0 : WireGuard {}; interface wg0 : WireGuard {};
zone lan_zone = { lan, wg0 }; zone lan_zone = { lan, wg0 };
``` ```
### 2. Import and Let ### Map literal
```fwl ```fwl
import rfc1918 : CIDRSet from "builtin:rfc1918";
let forwards : Map<(Protocol, Port), (IP, Port)> = { let forwards : Map<(Protocol, Port), (IP, Port)> = {
(tcp, :8080) -> (10.17.1.10, :80), (tcp, :8080) -> (10.17.1.10, :80),
(tcp, :2222) -> (10.17.1.11, :22) (tcp, :2222) -> (10.17.1.11, :22)
}; };
``` ```
### 3. Pattern and Flow ### Named patterns and flows
```fwl ```fwl
pattern WGInitiation : (UDPHeader, Bytes) = pattern WGInitiation : (UDPHeader, Bytes<{}>) =
(udp { length = 156 }, [0x01 _*]); (udp { length = 156 }, [0x01 _*]);
pattern WGResponse : (UDPHeader, Bytes) =
(udp { length = 100 }, [0x02 _*]);
flow WireGuardHandshake : FlowPattern = flow WireGuardHandshake : FlowPattern =
WGInitiation . WGResponse within 5s; WGInitiation . WGResponse within 5s;
``` ```
### 4. Rule with Effects ### Rule with effects
```fwl ```fwl
rule blockOutboundWG : Frame -> <FlowMatch, Log> Action = rule blockOutboundWG : Frame -> <FlowMatch, Log> Action =
@@ -497,9 +371,8 @@ rule blockOutboundWG : Frame -> <FlowMatch, Log> Action =
| Frame(iif in lan_zone -> wan, IPv4(ip, UDP(udp, payload))) | Frame(iif in lan_zone -> wan, IPv4(ip, UDP(udp, payload)))
if matches(WGInitiation, (udp, payload)) -> if matches(WGInitiation, (udp, payload)) ->
case perform FlowMatch.check(flowOf(ip, wg), WireGuardHandshake) of { case perform FlowMatch.check(flowOf(ip, wg), WireGuardHandshake) of {
| Matched -> | Matched -> do {
do { perform Log.emit(Warn, "WG blocked");
perform Log.emit(Warn, "WG blocked: " ++ show(ip.src));
Drop Drop
}; };
| _ -> Continue; | _ -> Continue;
@@ -508,190 +381,18 @@ rule blockOutboundWG : Frame -> <FlowMatch, Log> Action =
}; };
``` ```
### 5. Filter Policy ### Policy
```fwl ```fwl
policy input : Frame policy input : Frame
on { hook = Input, table = Filter, priority = Filter } on { hook = Input, table = Filter, priority = Filter } =
= { {
| _ if ct.state in { Established, Related } -> Allow; | _ if ct.state in { Established, Related } -> Allow;
| Frame(lo, _) -> Allow; | Frame(lo, _) -> Allow;
| Frame(_, IPv6(ip6, ICMPv6(_, _))) | Frame(_, Ether(_, IPv4(_, TCP(tcp, _))))
if ip6.src in fe80::/10 -> Allow;
| Frame(_, IPv4(_, TCP(tcp, _)))
if tcp.dport == :22 -> Allow; if tcp.dport == :22 -> Allow;
| Frame(_, IPv4(_, UDP(udp, _))) | Frame(_, Ether(_, IPv4(_, UDP(udp, _))))
if udp.dport == :51944 -> Allow; if udp.dport == :51944 -> Allow;
| _ -> Drop; | _ -> Drop;
}; };
``` ```
### 6. NAT Policy
```fwl
policy nat_prerouting : Frame
on { hook = Prerouting, table = NAT, priority = DstNat }
= {
| Frame(_, IPv4(ip, _)) ->
if perform FIB.daddrLocal(ip.dst)
then DNATMap(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;
};
```
### 7. Forward Policy calling a Rule
```fwl
policy forward : Frame
on { hook = Forward, table = Filter, priority = Filter }
= {
| _ if ct.state in { Established, Related } -> Allow;
| frame if iif in lan_zone && oif == wan -> blockOutboundWG(frame);
| _ if ct.status == DNAT -> Allow;
| Frame(iif in lan_zone -> wan, _) -> Allow;
| Frame(iif in lan_zone -> lan_zone, _) -> Allow;
| Frame(wan -> lan_zone, IPv4(ip, TCP(tcp, _)))
if (ip.dst, tcp.dport) in forwards -> Allow;
| _ -> Drop;
};
```
---
## Haskell AST Sketch
The following gives the direct mapping from grammar to Haskell types.
```haskell
-- src/FWL/AST.hs
data Program = Program [Config] [Decl]
data Config = Config { configTable :: Maybe String }
data Decl
= DInterface Name IfaceKind [IfaceProp]
| DZone Name [Name]
| DImport Name Type String
| DLet Name Type Expr
| DPattern Name Type PacketPat
| DFlow Name FlowExpr
| DRule Name Type Expr -- expr must be LamExpr
| DPolicy Name Type PolicyMeta ArmBlock
data PolicyMeta = PolicyMeta
{ pmHook :: Hook
, pmTable :: TableName
, pmPriority :: Priority }
data Hook = Input | Forward | Output | Prerouting | Postrouting
data TableName= Filter | NAT
data Priority = PFilter | PDstNat | PSrcNat | PMangle | PInt Int
data IfaceKind = WAN | LAN | WireGuard | IKUser Name
data IfaceProp = IPDynamic | IPCidr4 [CIDR] | IPCidr6 [CIDR]
-- Patterns
data Pat
= PWild
| PVar Name
| PNamed Name -- named pattern reference (first-class)
| PCtor Name [Pat]
| PRecord Name [FieldPat]
| PTuple [Pat]
| PFrame (Maybe PathPat) Pat
| PBytes [ByteElem]
data FieldPat
= FPEq Name Literal
| FPBind Name -- bind field to same-named var
| FPAs Name Name -- bind field to fresh var
data PathPat = PathPat (Maybe EndpointPat) (Maybe EndpointPat)
data EndpointPat
= EPWild
| EPName Name
| EPMember Name Name -- iif `in` zoneName
data ByteElem = BEHex Word8 | BEWild | BEWildStar
-- Flow
data FlowExpr
= FAtom Name
| FSeq FlowExpr FlowExpr (Maybe Duration)
type Duration = (Int, TimeUnit)
data TimeUnit = Seconds | Millis | Minutes | Hours
-- Types
data Type
= TName Name [Type]
| TTuple [Type]
| TFun Type Type
| TEffect [Name] Type
-- Expressions
data Expr
= EVar Name
| EQual [Name]
| ELit Literal
| ELam Name Expr
| EApp Expr Expr
| ECase Expr ArmBlock
| EIf Expr Expr Expr
| EDo [DoStmt]
| ELet Name Expr Expr
| ETuple [Expr]
| ESet [Expr]
| EMap [(Expr, Expr)]
| EPerform [Name] [Expr] -- perform QualName(args)
| EInfix InfixOp Expr Expr
| EPrefix PrefixOp Expr
data InfixOp
= OpAnd | OpOr | OpEq | OpNeq | OpLt | OpLte | OpGt | OpGte
| OpIn | OpConcat | OpThen | OpBind
data PrefixOp = OpNot
data DoStmt
= DSBind Name Expr
| DSExpr Expr
type ArmBlock = [Arm]
data Arm = Arm Pat (Maybe Expr) Expr -- pattern, guard, body
-- Actions (constructors of the Action type; parsed as Expr constructors)
-- Allow | Drop | Continue | Masquerade
-- | DNAT Expr | DNATMap Expr | Log LogLevel Expr
data LogLevel = LInfo | LWarn | LError
-- Literals
data Literal
= LInt Int
| LString String
| LBool Bool
| LIPv4 (Word8,Word8,Word8,Word8)
| LIPv6 [Word16]
| LCIDR Literal Int
| LPort Int
| LDuration Int TimeUnit
| LHex Word8
type Name = String
type CIDR = (Literal, Int)
```