Compare commits

...

4 commits

Author SHA1 Message Date
5489eeeb7a
Use correct output filename 2025-03-31 17:29:35 +02:00
39e1db9c32
Add basic firewall setup for l4lb namespace
Also teach sunet-l4lb-namespace to load the nft ruleset if it exists.
While here modify the script so instead of running "once per netns
config file" we merge the interface config from each json file into the
same dict per namespace. Without this we would attempt to load the nft
ruleset twice (once per file that mentioned the namespace) or warn twice
if the file did not exist etc.
2025-03-31 17:19:29 +02:00
f7dd464ed7
l4lb: install conntrack
Useful for veryfing what traffic is creating state.
2025-03-30 08:39:16 +02:00
db2b4ca409
Update sunet-l4lb-namespace
Make it able to delete addresses that are no longer in the netns config.
Also make it read one netns-base.json for hardware config which is
managed by puppet but also make it look for netns-sunet-cdn-agent.json
which is not created by puppet. This file will be generated by
sunet-cdn-agent and will include the configuration for dummy0.
2025-03-19 12:35:21 +01:00
4 changed files with 156 additions and 44 deletions

View file

@ -13,7 +13,9 @@ pylint sunet-l4lb-namespace
mypy --strict sunet-l4lb-namespace
"""
import ipaddress
import json
import os
import shlex
import subprocess
import sys
@ -21,6 +23,7 @@ import sys
def run_command(cmd: str) -> subprocess.CompletedProcess[str]:
"""Execute subprocess command"""
print(f"{cmd}")
args = shlex.split(cmd)
try:
proc = subprocess.run(args, capture_output=True, check=True, encoding="utf-8")
@ -35,12 +38,15 @@ def run_command(cmd: str) -> subprocess.CompletedProcess[str]:
return proc
def configure_interfaces(
def configure_interfaces( # pylint: disable=too-many-locals,too-many-branches
namespace: str, if_data: dict[str, dict[str, list[str]]]
) -> None:
"""Configure interfaces"""
proc = run_command("ip netns exec l4lb ip -j addr show")
proc = run_command(f"ip netns exec {namespace} ip -j addr show")
namespace_ifs = json.loads(proc.stdout)
ipv4_key = "ipv4"
ipv6_key = "ipv6"
for if_name, data in if_data.items():
if_exists = next(
(True for interface in namespace_ifs if interface["ifname"] == if_name),
@ -54,38 +60,73 @@ def configure_interfaces(
else:
run_command(f"ip link set {if_name} netns {namespace}")
run_command(f"ip netns exec {namespace} ip link set {if_name} up")
proc = run_command(f"ip netns exec {namespace} ip -j addr show dev {if_name}")
if_conf = json.loads(proc.stdout)
for ipv4_cidr in data["ipv4"]:
ip4, prefix = ipv4_cidr.split("/")
v4_addr_exists = next(
(
True
for addr in if_conf[0]["addr_info"]
if addr["local"] == ip4 and addr["prefixlen"] == int(prefix)
),
False,
)
if not v4_addr_exists:
run_command(
f"ip netns exec {namespace} ip addr add {ipv4_cidr} dev {if_name}"
# Add missing addresses from config
if ipv4_key in data:
for configured_ipv4_cidr in data[ipv4_key]:
ip4, prefix = configured_ipv4_cidr.split("/")
v4_addr_exists = next(
(
True
for addr in if_conf[0]["addr_info"]
if addr["local"] == ip4 and addr["prefixlen"] == int(prefix)
),
False,
)
for ipv6_cidr in data["ipv6"]:
ip6, prefix = ipv6_cidr.split("/")
v6_addr_exists = next(
(
True
for addr in if_conf[0]["addr_info"]
if addr["local"] == ip6 and addr["prefixlen"] == int(prefix)
),
False,
)
if not v6_addr_exists:
run_command(
f"ip netns exec {namespace} ip addr add {ipv6_cidr} dev {if_name}"
if not v4_addr_exists:
run_command(
f"ip netns exec {namespace} ip addr add {configured_ipv4_cidr} dev {if_name}" # pylint: disable=line-too-long
)
if ipv6_key in data:
for ipv6_cidr in data[ipv6_key]:
ip6, prefix = ipv6_cidr.split("/")
v6_addr_exists = next(
(
True
for addr in if_conf[0]["addr_info"]
if addr["local"] == ip6 and addr["prefixlen"] == int(prefix)
),
False,
)
if not v6_addr_exists:
run_command(
f"ip netns exec {namespace} ip addr add {ipv6_cidr} dev {if_name}"
)
# Remove no longer configured addresseses
for addr_info in if_conf[0]["addr_info"]:
# Ignore addresses like fe80
if addr_info["scope"] != "global":
continue
cidr = "/".join((addr_info["local"], str(addr_info["prefixlen"])))
# We need strict=False because otherwise ip_network() gets angry if
# there are host bits set in the address (which of course there is
# because we are parsing actual interface configs, not pure
# "networks")
cidr_net = ipaddress.ip_network(cidr, strict=False)
needs_removal = False
if cidr_net.version == 4:
if ipv4_key not in data or cidr not in data[ipv4_key]:
needs_removal = True
elif cidr_net.version == 6:
if ipv6_key not in data or cidr not in data[ipv6_key]:
needs_removal = True
else:
raise ValueError(
f"Expected IPv4 or IPv6, got something else: {cidr_net.version}"
)
run_command(f"ip netns exec {namespace} ip link set {if_name} up")
if needs_removal:
run_command(
f"ip netns exec {namespace} ip addr del {cidr} dev {if_name}"
)
def setup_namespaces(netns_data: dict[str, dict[str, dict[str, list[str]]]]) -> None:
@ -99,8 +140,17 @@ def setup_namespaces(netns_data: dict[str, dict[str, dict[str, list[str]]]]) ->
if not netns_exists:
run_command(f"ip netns add {namespace}")
# Make localhost available
run_command(f"ip netns exec {namespace} ip link set lo up")
# Make localhost available
run_command(f"ip netns exec {namespace} ip link set lo up")
# (Re)load the nft ruleset for the given namespace
nft_ruleset = f"/opt/sunet-cdn/l4lb/conf/nft-{namespace}.conf"
if os.path.isfile(nft_ruleset):
run_command(f"ip netns exec {namespace} nft -f {nft_ruleset}")
else:
print(
f"WARNING: no nft ruleset found for namespace '{namespace}' ({nft_ruleset}), the namespace will not be firewalled" # pylint: disable=line-too-long
)
configure_interfaces(namespace, if_data)
@ -129,10 +179,30 @@ def main() -> None:
# }
# }
# }
with open("/opt/sunet-cdn/l4lb/conf/netns.json", encoding="utf-8") as f:
netns_data = json.load(f)
setup_namespaces(netns_data)
input_files = [
"/opt/sunet-cdn/l4lb/conf/netns-base.json",
"/opt/sunet-cdn/l4lb/conf/netns-sunet-cdn-agent.json",
]
merged_netns_data: dict[str, dict[str, dict[str, list[str]]]] = {}
for input_file in input_files:
try:
with open(input_file, encoding="utf-8") as f:
netns_data = json.load(f)
# Combine interface config from multiple files belonging to the same namespace
for ns, ns_data in netns_data.items():
if ns in merged_netns_data:
merged_netns_data[ns].update(ns_data)
else:
merged_netns_data[ns] = ns_data
except FileNotFoundError:
print(f"skipping nonexistant file '{input_file}'")
continue
setup_namespaces(merged_netns_data)
if __name__ == "__main__":

View file

@ -17,6 +17,8 @@ class cdn::l4lb(
include sunet::systemd_reload
package {'conntrack': ensure => installed }
package {'bird2': ensure => installed }
file { '/opt/sunet-cdn':
@ -54,12 +56,20 @@ class cdn::l4lb(
mode => '0640',
}
file { '/opt/sunet-cdn/l4lb/conf/netns.json':
file { '/opt/sunet-cdn/l4lb/conf/netns-base.json':
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
content => template('cdn/l4lb/netns.json.erb'),
content => template('cdn/l4lb/netns-base.json.erb'),
}
file { '/opt/sunet-cdn/l4lb/conf/nft-l4lb.conf':
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
content => template('cdn/l4lb/nft-l4lb.conf.erb'),
}
file { '/usr/local/bin/sunet-l4lb-namespace':

View file

@ -15,14 +15,6 @@
"ipv6": [
"2001:6b0:2006:75::1/127"
]
},
"dummy0": {
"ipv4": [
"188.240.152.1/32"
],
"ipv6": [
"2001:6b0:2100::1/128"
]
}
}
}

View file

@ -0,0 +1,40 @@
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
# accept any localhost traffic
iif lo counter accept
# accept icmp
ip protocol icmp counter accept
ip6 nexthdr icmpv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded,
parameter-problem, echo-request, mld-listener-query,
nd-router-solicit, nd-router-advert, nd-neighbor-solicit,
nd-neighbor-advert } counter accept
# accept traffic originated from us
ct state established counter accept
# silently drop invalid packets
ct state invalid counter drop
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0;
}
}
# HTTP and HTTPS
add rule inet filter input tcp dport 80 counter accept comment "l4lb HTTP"
add rule inet filter input tcp dport 443 counter accept comment "l4lb HTTPS"
# BGP
add rule inet filter input ip saddr { 130.242.64.232 } tcp dport 179 counter accept comment "tug-r11-v4"
add rule inet filter input ip saddr { 130.242.64.234 } tcp dport 179 counter accept comment "tug-r12-v4"
add rule inet filter input ip6 saddr { 2001:6b0:2006:74:: } tcp dport 179 counter accept comment "tug-r11-v6"
add rule inet filter input ip6 saddr { 2001:6b0:2006:75:: } tcp dport 179 counter accept comment "tug-r12-v6"