Compare commits

..

No commits in common. "main" and "cdn-ops-2024-11-14-v02" have entirely different histories.

7 changed files with 66 additions and 193 deletions

View file

@ -1,25 +1,10 @@
#!/bin/bash #!/bin/bash
set -e set -e
# shellcheck source=/dev/null
. /conf/init-cdn-db.conf . /conf/init-cdn-db.conf
# Create database named after user, then create a schema named the same as the
# user which is also owned by that user. Because search_path (SHOW
# search_path;) starts with "$user" by default this means any tables will be
# created in that user-specific SCHEMA by default instead of falling back to
# "public". This follows the "secure schema usage pattern" summarized as
# "Constrain ordinary users to user-private schemas" from
# https://www.postgresql.org/docs/current/ddl-schemas.html#DDL-SCHEMAS-PATTERNS
#
# "In PostgreSQL 15 and later, the default configuration supports this usage
# pattern. In prior versions, or when using a database that has been upgraded
# from a prior version, you will need to remove the public CREATE privilege
# from the public schema"
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER cdn WITH PASSWORD '${cdn_password:?}'; CREATE USER cdn WITH PASSWORD '$cdn_password';
CREATE DATABASE cdn; CREATE DATABASE cdn;
GRANT ALL PRIVILEGES ON DATABASE cdn TO cdn; GRANT ALL PRIVILEGES ON DATABASE cdn TO cdn;
\c cdn;
CREATE SCHEMA cdn AUTHORIZATION cdn;
EOSQL EOSQL

View file

@ -13,9 +13,7 @@ pylint sunet-l4lb-namespace
mypy --strict sunet-l4lb-namespace mypy --strict sunet-l4lb-namespace
""" """
import ipaddress
import json import json
import os
import shlex import shlex
import subprocess import subprocess
import sys import sys
@ -23,7 +21,6 @@ import sys
def run_command(cmd: str) -> subprocess.CompletedProcess[str]: def run_command(cmd: str) -> subprocess.CompletedProcess[str]:
"""Execute subprocess command""" """Execute subprocess command"""
print(f"{cmd}")
args = shlex.split(cmd) args = shlex.split(cmd)
try: try:
proc = subprocess.run(args, capture_output=True, check=True, encoding="utf-8") proc = subprocess.run(args, capture_output=True, check=True, encoding="utf-8")
@ -38,15 +35,12 @@ def run_command(cmd: str) -> subprocess.CompletedProcess[str]:
return proc return proc
def configure_interfaces( # pylint: disable=too-many-locals,too-many-branches def configure_interfaces(
namespace: str, if_data: dict[str, dict[str, list[str]]] namespace: str, if_data: dict[str, dict[str, list[str]]]
) -> None: ) -> None:
"""Configure interfaces""" """Configure interfaces"""
proc = run_command(f"ip netns exec {namespace} ip -j addr show") proc = run_command("ip netns exec l4lb ip -j addr show")
namespace_ifs = json.loads(proc.stdout) namespace_ifs = json.loads(proc.stdout)
ipv4_key = "ipv4"
ipv6_key = "ipv6"
for if_name, data in if_data.items(): for if_name, data in if_data.items():
if_exists = next( if_exists = next(
(True for interface in namespace_ifs if interface["ifname"] == if_name), (True for interface in namespace_ifs if interface["ifname"] == if_name),
@ -60,73 +54,38 @@ def configure_interfaces( # pylint: disable=too-many-locals,too-many-branches
else: else:
run_command(f"ip link set {if_name} netns {namespace}") 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}") proc = run_command(f"ip netns exec {namespace} ip -j addr show dev {if_name}")
if_conf = json.loads(proc.stdout) if_conf = json.loads(proc.stdout)
for ipv4_cidr in data["ipv4"]:
# Add missing addresses from config ip4, prefix = ipv4_cidr.split("/")
if ipv4_key in data: v4_addr_exists = next(
for configured_ipv4_cidr in data[ipv4_key]: (
ip4, prefix = configured_ipv4_cidr.split("/") True
v4_addr_exists = next( for addr in if_conf[0]["addr_info"]
( if addr["local"] == ip4 and addr["prefixlen"] == int(prefix)
True ),
for addr in if_conf[0]["addr_info"] False,
if addr["local"] == ip4 and addr["prefixlen"] == int(prefix) )
), if not v4_addr_exists:
False,
)
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}"
)
if needs_removal:
run_command( run_command(
f"ip netns exec {namespace} ip addr del {cidr} dev {if_name}" f"ip netns exec {namespace} ip addr add {ipv4_cidr} dev {if_name}"
) )
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}"
)
run_command(f"ip netns exec {namespace} ip link set {if_name} up")
def setup_namespaces(netns_data: dict[str, dict[str, dict[str, list[str]]]]) -> None: def setup_namespaces(netns_data: dict[str, dict[str, dict[str, list[str]]]]) -> None:
@ -140,17 +99,8 @@ def setup_namespaces(netns_data: dict[str, dict[str, dict[str, list[str]]]]) ->
if not netns_exists: if not netns_exists:
run_command(f"ip netns add {namespace}") run_command(f"ip netns add {namespace}")
# Make localhost available # Make localhost available
run_command(f"ip netns exec {namespace} ip link set lo up") 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) configure_interfaces(namespace, if_data)
@ -179,30 +129,10 @@ def main() -> None:
# } # }
# } # }
# } # }
with open("/opt/sunet-cdn/l4lb/conf/netns.json", encoding="utf-8") as f:
netns_data = json.load(f)
input_files = [ setup_namespaces(netns_data)
"/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__": if __name__ == "__main__":

View file

@ -3,7 +3,7 @@ class cdn::cache(
Hash[String, Integer] $customers = { Hash[String, Integer] $customers = {
customer1 => 1000000000, customer1 => 1000000000,
}, },
String $sunet_cdn_purger_version = '0.0.8', String $sunet_cdnp_version = '0.0.6',
Hash[String, String] $acme_url = { Hash[String, String] $acme_url = {
test => 'https://internal-sto3-test-ca-1.cdn.sunet.se:9000/acme/acme/directory' test => 'https://internal-sto3-test-ca-1.cdn.sunet.se:9000/acme/acme/directory'
}, },
@ -155,51 +155,51 @@ class cdn::cache(
creates => "/etc/letsencrypt/live/${my_fqdn}/fullchain.pem" creates => "/etc/letsencrypt/live/${my_fqdn}/fullchain.pem"
} }
$sunet_cdn_purger_dir = '/var/lib/sunet-cdn-purger' $sunet_cdnp_dir = '/var/lib/sunet-cdnp'
$sunet_cdn_purger_file = "sunet-cdn-purger_${sunet_cdn_purger_version}_linux_${facts[os][architecture]}.tar.gz" $sunet_cdnp_file = "sunet-cdnp_${sunet_cdnp_version}_linux_${facts[os][architecture]}.tar.gz"
$sunet_cdn_purger_url = "https://github.com/SUNET/sunet-cdn-purger/releases/download/v${sunet_cdn_purger_version}/${sunet_cdn_purger_file}" $sunet_cdnp_url = "https://github.com/SUNET/sunet-cdnp/releases/download/v${sunet_cdnp_version}/${sunet_cdnp_file}"
# Create directory for managing CDP purger # Create directory for managing CDP purger
file { $sunet_cdn_purger_dir: file { $sunet_cdnp_dir:
ensure => directory, ensure => directory,
owner => 'root', owner => 'root',
group => 'root', group => 'root',
mode => '0755', mode => '0755',
} }
exec { "curl -LO ${sunet_cdn_purger_url}": exec { "curl -LO ${sunet_cdnp_url}":
creates => "${sunet_cdn_purger_dir}/${sunet_cdn_purger_file}", creates => "${sunet_cdnp_dir}/${sunet_cdnp_file}",
cwd => $sunet_cdn_purger_dir, cwd => $sunet_cdnp_dir,
notify => Exec['extract sunet-cdn-purger'], notify => Exec['extract sunet-cdnp'],
} }
exec { 'extract sunet-cdn-purger': exec { 'extract sunet-cdnp':
command => "tar -xzf ${sunet_cdn_purger_file} sunet-cdn-purger", command => "tar -xzf ${sunet_cdnp_file} sunet-cdnp",
cwd => $sunet_cdn_purger_dir, cwd => $sunet_cdnp_dir,
refreshonly => true, refreshonly => true,
notify => Service['sunet-cdn-purger'], notify => Service['sunet-cdnp'],
} }
file { "${sunet_cdn_purger_dir}/sunet-cdn-purger": file { "${sunet_cdnp_dir}/sunet-cdnp":
owner => 'root', owner => 'root',
group => 'root', group => 'root',
mode => '0755', mode => '0755',
} }
file { '/usr/local/bin/sunet-cdn-purger': file { '/usr/local/bin/sunet-cdnp':
ensure => link, ensure => link,
target => "${sunet_cdn_purger_dir}/sunet-cdn-purger", target => "${sunet_cdnp_dir}/sunet-cdnp",
} }
file { '/etc/systemd/system/sunet-cdn-purger.service': file { '/etc/systemd/system/sunet-cdnp.service':
ensure => file, ensure => file,
owner => 'root', owner => 'root',
group => 'root', group => 'root',
mode => '0644', mode => '0644',
content => template('cdn/cache/sunet-cdn-purger.service.erb'), content => template('cdn/cache/sunet-cdnp.service.erb'),
notify => [Class['sunet::systemd_reload']], notify => [Class['sunet::systemd_reload']],
} }
service { 'sunet-cdn-purger': service { 'sunet-cdnp':
ensure => 'running', ensure => 'running',
enable => true, enable => true,
} }

View file

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

View file

@ -6,7 +6,7 @@ After=docker.service
[Service] [Service]
Type=simple Type=simple
ExecStart=/usr/local/bin/sunet-cdn-purger \ ExecStart=/usr/local/bin/sunet-cdnp \
-mqtt-ca-file /usr/local/share/ca-certificates/step_ca_root.crt \ -mqtt-ca-file /usr/local/share/ca-certificates/step_ca_root.crt \
-mqtt-client-key-file /etc/letsencrypt/live/<%= @networking['fqdn'] %>/privkey.pem \ -mqtt-client-key-file /etc/letsencrypt/live/<%= @networking['fqdn'] %>/privkey.pem \
-mqtt-client-cert-file /etc/letsencrypt/live/<%= @networking['fqdn'] %>/fullchain.pem \ -mqtt-client-cert-file /etc/letsencrypt/live/<%= @networking['fqdn'] %>/fullchain.pem \

View file

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

View file

@ -1,40 +0,0 @@
#!/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"