feat(filter_rule): Add complete input + output + forward (without dest)
This commit is contained in:
parent
cf7903c9c3
commit
dde922f888
3 changed files with 136 additions and 80 deletions
|
@ -13,6 +13,9 @@ zones:
|
||||||
negate: true
|
negate: true
|
||||||
zones: [adm, mgmt]
|
zones: [adm, mgmt]
|
||||||
|
|
||||||
|
interco-crans:
|
||||||
|
addrs: 10.0.0.1/32
|
||||||
|
|
||||||
|
|
||||||
blacklist:
|
blacklist:
|
||||||
blocked: adm
|
blocked: adm
|
||||||
|
@ -24,14 +27,16 @@ reverse_path_filter:
|
||||||
|
|
||||||
filter:
|
filter:
|
||||||
input:
|
input:
|
||||||
- src: internet
|
- iif: lo
|
||||||
dst: gitea
|
|
||||||
protocols:
|
|
||||||
tcp:
|
|
||||||
dport: 22
|
|
||||||
verdict: accept
|
verdict: accept
|
||||||
|
|
||||||
- iif: lo
|
- src: adm
|
||||||
|
protocols:
|
||||||
|
icmp: true
|
||||||
|
ospf: true
|
||||||
|
vrrp: true
|
||||||
|
tcp:
|
||||||
|
dport: 179
|
||||||
verdict: accept
|
verdict: accept
|
||||||
|
|
||||||
- src: mgmt
|
- src: mgmt
|
||||||
|
@ -40,39 +45,33 @@ filter:
|
||||||
dport: [22, 240..242]
|
dport: [22, 240..242]
|
||||||
verdict: accept
|
verdict: accept
|
||||||
|
|
||||||
#
|
- protocols:
|
||||||
# - src: backbone
|
icmp: true
|
||||||
# protocols:
|
verdict: accept
|
||||||
# ospf: true
|
|
||||||
# vrrp: true
|
output:
|
||||||
# tcp:
|
- verdict: accept
|
||||||
# dport: [179]
|
|
||||||
# verdict: accept
|
|
||||||
#
|
forward:
|
||||||
# - protocols:
|
- src: interco-crans
|
||||||
# icmp: true
|
verdict: accept
|
||||||
# verdict: accept
|
|
||||||
#
|
- src: users-internet-allowed
|
||||||
# output:
|
protocols:
|
||||||
# - verdict: accept
|
tcp:
|
||||||
#
|
dport: [25]
|
||||||
# forward:
|
verdict: drop
|
||||||
# - src: interco-crans
|
|
||||||
# verdict: accept
|
- src: users-internet-allowed
|
||||||
#
|
|
||||||
# - src: users-internet-allowed
|
|
||||||
# protocols:
|
|
||||||
# tcp:
|
|
||||||
# dport: [25]
|
|
||||||
# verdict: drop
|
|
||||||
#
|
|
||||||
# - src: users-internet-allowed
|
|
||||||
# dest: [10.0.0.1, internet]
|
# dest: [10.0.0.1, internet]
|
||||||
# verdict: accept
|
verdict: accept
|
||||||
|
|
||||||
|
# TODO: Nat translation
|
||||||
#
|
#
|
||||||
# nat:
|
# nat:
|
||||||
# - src: mgmt
|
# - src: mgmt
|
||||||
# snat:
|
# snat:
|
||||||
# addr: 45.66.108.14
|
# addr: 45.66.108.14
|
||||||
# persistent: true
|
# persistent: true
|
||||||
...
|
...
|
||||||
|
|
137
firewall.py
137
firewall.py
|
@ -64,13 +64,9 @@ class PortRange(str):
|
||||||
start, end = v.split("..")
|
start, end = v.split("..")
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
parse_obj_as(Port, v) # This is the expected error
|
parse_obj_as(Port, v) # This is the expected error
|
||||||
raise ValueError(
|
raise ValueError("invalid port range: must be in the form start..end")
|
||||||
"invalid port range: must be in the form start..end"
|
|
||||||
)
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError(
|
raise ValueError("invalid port range: must be in the form start..end")
|
||||||
"invalid port range: must be in the form start..end"
|
|
||||||
)
|
|
||||||
|
|
||||||
start, end = parse_obj_as(Port, start), parse_obj_as(Port, end)
|
start, end = parse_obj_as(Port, start), parse_obj_as(Port, end)
|
||||||
if start > end:
|
if start > end:
|
||||||
|
@ -204,9 +200,7 @@ def resolve_zones(yaml_zones: dict[ZoneName, ZoneEntry]) -> Zones:
|
||||||
|
|
||||||
for name in TopologicalSorter(zone_graph).static_order():
|
for name in TopologicalSorter(zone_graph).static_order():
|
||||||
if yaml_zones[name].addrs:
|
if yaml_zones[name].addrs:
|
||||||
zones[name] = ResolvedZone(
|
zones[name] = ResolvedZone(yaml_zones[name].addrs, yaml_zones[name].negate)
|
||||||
yaml_zones[name].addrs, yaml_zones[name].negate
|
|
||||||
)
|
|
||||||
|
|
||||||
elif yaml_zones[name].file is not None:
|
elif yaml_zones[name].file is not None:
|
||||||
with open(yaml_zones[name].file, "r") as file:
|
with open(yaml_zones[name].file, "r") as file:
|
||||||
|
@ -217,9 +211,7 @@ def resolve_zones(yaml_zones: dict[ZoneName, ZoneEntry]) -> Zones:
|
||||||
f"YAML parsing failed of the included file '{yaml_zones[name].file}': {e}"
|
f"YAML parsing failed of the included file '{yaml_zones[name].file}': {e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
zones[name] = ResolvedZone(
|
zones[name] = ResolvedZone(yaml_addrs.__root__, yaml_zones[name].negate)
|
||||||
yaml_addrs.__root__, yaml_zones[name].negate
|
|
||||||
)
|
|
||||||
|
|
||||||
elif yaml_zones[name].zones:
|
elif yaml_zones[name].zones:
|
||||||
addrs: set[IPvAnyNetwork] = set()
|
addrs: set[IPvAnyNetwork] = set()
|
||||||
|
@ -269,7 +261,10 @@ def zones_into_ip(
|
||||||
for element in elements:
|
for element in elements:
|
||||||
match element:
|
match element:
|
||||||
case ZoneName():
|
case ZoneName():
|
||||||
zone = zones[element]
|
try:
|
||||||
|
zone = zones[element]
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError(f"zone '{element}' does not exist")
|
||||||
|
|
||||||
if not allow_negate and zone.negate:
|
if not allow_negate and zone.negate:
|
||||||
raise ValueError(f"zone '{element}' cannot be negated")
|
raise ValueError(f"zone '{element}' cannot be negated")
|
||||||
|
@ -370,46 +365,83 @@ def parse_reverse_path_filter(rpf: ReversePathFilter) -> nft.Table:
|
||||||
return table
|
return table
|
||||||
|
|
||||||
|
|
||||||
# Create a chain "input_filter" and for each rule from the DSL:
|
# Create a chain "{hook}_filter" and for each rule from the DSL:
|
||||||
# - Create a specific chain "input_rules_{i}"
|
# - Create a specific chain "{hook}_rules_{i}"
|
||||||
# - Add a rule to "input_filter" that jumps to chain "input_rules_{i}"
|
# - If needed, add a network range in the set "{hook}_set_{i}"
|
||||||
def parse_filter_input(rules: list[Rule], zones: Zones) -> list[nft.Chain]:
|
# - Add a rule to "input_filter" that jumps to chain "{hook}_rules_{i}"
|
||||||
|
def parse_filter_rules(hook: str, rules: list[Rule], zones: Zones) -> list[nft.Chain]:
|
||||||
|
# Container of every "{hook}_set_{i}"
|
||||||
all_chains = []
|
all_chains = []
|
||||||
|
|
||||||
chain_input = nft.Chain(
|
# The chain "{hook}_filter"
|
||||||
name="input_filter",
|
chain_hook = nft.Chain(
|
||||||
|
name=f"{hook}_filter",
|
||||||
type="filter",
|
type="filter",
|
||||||
hook="input",
|
hook=hook,
|
||||||
policy="drop",
|
policy="drop", # TODO: Correct default policy
|
||||||
priority=0,
|
priority=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
for i, rule in enumerate(rules):
|
for i, rule in enumerate(rules):
|
||||||
chain_spec_rules: list[nft.Statement] = []
|
# Container of v4/v6 zones rules of "{hook}_rules_{i}", if needed
|
||||||
chain_input_rules: list[nft.Statement] = []
|
v4: nft.Match | None = None
|
||||||
|
v6: nft.Match | None = None
|
||||||
|
|
||||||
# Input interface: chain "input_filter"
|
# Container of rules of "{hook}_filter"
|
||||||
|
chain_hook_rules: list[nft.Statement] = []
|
||||||
|
|
||||||
|
# Container of specific rules of "{hook}_rules_{i}"
|
||||||
|
chain_spec_rules: list[nft.Statement] = []
|
||||||
|
|
||||||
|
# Input/Output interface: chain "{hook}_filter"
|
||||||
if rule.iif is not None:
|
if rule.iif is not None:
|
||||||
chain_input_rules.append(
|
chain_hook_rules.append(
|
||||||
nft.Match(
|
nft.Match(
|
||||||
op="==",
|
op="==",
|
||||||
left=nft.Meta("iif"),
|
left=nft.Meta("iifname"),
|
||||||
right=nft.Immediate(rule.iif),
|
right=nft.Immediate(rule.iif),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Protocols (ICMP/OSPF/VRRP): chain "input_filter"
|
if rule.oif is not None:
|
||||||
|
chain_hook_rules.append(
|
||||||
|
nft.Match(
|
||||||
|
op="==",
|
||||||
|
left=nft.Meta("oifname"),
|
||||||
|
right=nft.Immediate(rule.oif),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Source/Destination: chain "{hook}_filter"
|
||||||
|
if rule.src is not None:
|
||||||
|
ip_v4, ip_v6 = split_v4_v6(zones_into_ip(rule.src, zones))
|
||||||
|
|
||||||
|
if ip_v4:
|
||||||
|
v4 = nft.Match(
|
||||||
|
op="==",
|
||||||
|
left=nft.Payload(protocol="ip", field="saddr"),
|
||||||
|
right=nft.Immediate(ip_v4),
|
||||||
|
)
|
||||||
|
|
||||||
|
if ip_v6:
|
||||||
|
v6 = nft.Match(
|
||||||
|
op="==",
|
||||||
|
left=nft.Payload(protocol="ip6", field="saddr"),
|
||||||
|
right=nft.Immediate(ip_v6),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Protocols (ICMP/OSPF/VRRP): chain "{hook}_rules_{i}"
|
||||||
protocols_v4 = set()
|
protocols_v4 = set()
|
||||||
protocols_v6 = set()
|
protocols_v6 = set()
|
||||||
|
|
||||||
for v4, v6 in [
|
for protoname_v4, protoname_v6 in [
|
||||||
("icmp", "ipv6-icmp"),
|
("icmp", "ipv6-icmp"),
|
||||||
("ospf", "ospf"),
|
("ospf", "ospf"),
|
||||||
("vrrp", "vrrp"),
|
("vrrp", "vrrp"),
|
||||||
]:
|
]:
|
||||||
if rule.protocols[v4]:
|
if rule.protocols[protoname_v4]:
|
||||||
protocols_v4.add(v4)
|
protocols_v4.add(protoname_v4)
|
||||||
protocols_v6.add(v6)
|
protocols_v6.add(protoname_v6)
|
||||||
|
|
||||||
if protocols_v4:
|
if protocols_v4:
|
||||||
chain_spec_rules.append(
|
chain_spec_rules.append(
|
||||||
|
@ -427,7 +459,7 @@ def parse_filter_input(rules: list[Rule], zones: Zones) -> list[nft.Chain]:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Protocol UDP/TCP: chain "input_filter"
|
# Protocol UDP/TCP: chain "{hook}_rules_{i}"
|
||||||
for proto, port in [
|
for proto, port in [
|
||||||
("udp", "dport"),
|
("udp", "dport"),
|
||||||
("udp", "sport"),
|
("udp", "sport"),
|
||||||
|
@ -445,7 +477,7 @@ def parse_filter_input(rules: list[Rule], zones: Zones) -> list[nft.Chain]:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verdict: specific chain "input_rules_{i}"
|
# Verdict: specific chain "{hook}_rules_{i}"
|
||||||
if rule.verdict == Verdict.accept:
|
if rule.verdict == Verdict.accept:
|
||||||
rules_verdict = nft.Verdict("accept")
|
rules_verdict = nft.Verdict("accept")
|
||||||
elif rule.verdict == Verdict.drop:
|
elif rule.verdict == Verdict.drop:
|
||||||
|
@ -453,19 +485,34 @@ def parse_filter_input(rules: list[Rule], zones: Zones) -> list[nft.Chain]:
|
||||||
elif rule.verdict == Verdict.reject:
|
elif rule.verdict == Verdict.reject:
|
||||||
rules_verdict = nft.Verdict("reject")
|
rules_verdict = nft.Verdict("reject")
|
||||||
|
|
||||||
# Create the chain "input_rules_{i}"
|
# Create the chain "{hook}_rules_{i}"
|
||||||
chain = nft.Chain(name=f"input_rules_{i}")
|
chain = nft.Chain(name=f"{hook}_rules_{i}")
|
||||||
|
|
||||||
for spec_rule in chain_spec_rules:
|
for spec_rule in chain_spec_rules:
|
||||||
chain.rules.append(nft.Rule([spec_rule, rules_verdict]))
|
chain.rules.append(nft.Rule([spec_rule, rules_verdict]))
|
||||||
|
|
||||||
|
# TODO: Is it mandatory when `chain_spec_rules` is not empty?
|
||||||
|
chain.rules.append(nft.Rule([rules_verdict]))
|
||||||
|
|
||||||
all_chains.append(chain)
|
all_chains.append(chain)
|
||||||
|
|
||||||
# Add the chain "input_rules_{i}" to the chain "input_filter"
|
# Add the chain "{hook}_rules_{i}" to the chain "{hook}_filter"
|
||||||
chain_input_rules.append(nft.Goto(f"input_rules_{i}"))
|
if v4 is not None:
|
||||||
chain_input.rules.append(nft.Rule(chain_input_rules))
|
chain_hook.rules.append(nft.Rule(
|
||||||
|
[v4] + chain_hook_rules + [nft.Goto(f"{hook}_rules_{i}")]
|
||||||
|
))
|
||||||
|
|
||||||
return all_chains + [chain_input]
|
if v6 is not None:
|
||||||
|
chain_hook.rules.append(nft.Rule(
|
||||||
|
[v6] + chain_hook_rules + [nft.Goto(f"{hook}_rules_{i}")]
|
||||||
|
))
|
||||||
|
|
||||||
|
if v4 is None and v6 is None:
|
||||||
|
chain_hook.rules.append(nft.Rule(
|
||||||
|
chain_hook_rules + [nft.Goto(f"{hook}_rules_{i}")]
|
||||||
|
))
|
||||||
|
|
||||||
|
return all_chains + [chain_hook]
|
||||||
|
|
||||||
|
|
||||||
def parse_filter(filter: Filter, zones: Zones) -> nft.Table:
|
def parse_filter(filter: Filter, zones: Zones) -> nft.Table:
|
||||||
|
@ -491,14 +538,20 @@ def parse_filter(filter: Filter, zones: Zones) -> nft.Table:
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Inputs
|
# Input/Output/Forward chains
|
||||||
chains_inputs = parse_filter_input(filter.input, zones)
|
chains_input = parse_filter_rules("input", filter.input, zones)
|
||||||
|
chains_output = parse_filter_rules("output", filter.output, zones)
|
||||||
|
|
||||||
|
# TODO: dest rule in ForwardRule
|
||||||
|
chains_forward = parse_filter_rules("forward", filter.forward, zones)
|
||||||
|
|
||||||
# Resulting table
|
# Resulting table
|
||||||
table = nft.Table(name="filter", family="inet")
|
table = nft.Table(name="filter", family="inet")
|
||||||
|
|
||||||
table.chains.extend([chain_conntrack])
|
table.chains.extend([chain_conntrack])
|
||||||
table.chains.extend(chains_inputs)
|
table.chains.extend(chains_input)
|
||||||
|
table.chains.extend(chains_output)
|
||||||
|
table.chains.extend(chains_forward)
|
||||||
|
|
||||||
return table
|
return table
|
||||||
|
|
||||||
|
|
4
nft.py
4
nft.py
|
@ -44,6 +44,10 @@ class Immediate(Generic[T]):
|
||||||
return ip_to_nft(self.value)
|
return ip_to_nft(self.value)
|
||||||
|
|
||||||
if isinstance(self.value, set):
|
if isinstance(self.value, set):
|
||||||
|
for elem in self.value:
|
||||||
|
if isinstance(elem, Immediate):
|
||||||
|
return {"set": [elem.to_nft() for elem in self.value]}
|
||||||
|
|
||||||
return {"set": list(self.value)}
|
return {"set": list(self.value)}
|
||||||
|
|
||||||
return self.value
|
return self.value
|
||||||
|
|
Loading…
Reference in a new issue