From f7d0a9150d794834c98addef3240cde98551a202 Mon Sep 17 00:00:00 2001 From: missytake Date: Thu, 11 Sep 2025 18:59:33 +0200 Subject: [PATCH] proxy: untested draft about deploying a reverse proxy --- cmdeploy/src/cmdeploy/cmdeploy.py | 55 +++++++++++++++ cmdeploy/src/cmdeploy/proxy-deploy.py | 19 ++++++ cmdeploy/src/cmdeploy/proxy.py | 63 +++++++++++++++++ .../proxy_files/60-floating.ip.cfg.j2 | 4 ++ .../src/cmdeploy/proxy_files/nftables.conf.j2 | 67 +++++++++++++++++++ cmdeploy/src/cmdeploy/remote/rshell.py | 14 ++++ 6 files changed, 222 insertions(+) create mode 100644 cmdeploy/src/cmdeploy/proxy-deploy.py create mode 100644 cmdeploy/src/cmdeploy/proxy.py create mode 100644 cmdeploy/src/cmdeploy/proxy_files/60-floating.ip.cfg.j2 create mode 100644 cmdeploy/src/cmdeploy/proxy_files/nftables.conf.j2 diff --git a/cmdeploy/src/cmdeploy/cmdeploy.py b/cmdeploy/src/cmdeploy/cmdeploy.py index 8ae2481b..88c75c52 100644 --- a/cmdeploy/src/cmdeploy/cmdeploy.py +++ b/cmdeploy/src/cmdeploy/cmdeploy.py @@ -208,6 +208,61 @@ def test_cmd(args, out): return ret +def proxy_cmd_options(parser: argparse.ArgumentParser): + parser.add_argument( + "ip_address", + help="specify a server to deploy to; can also be an inventory.py file", + ) + parser.add_argument( + "--relay-ipv4", + dest="relay_ipv4", + help="The ipv4 address of the relay you want to forward traffic to", + ) + parser.add_argument( + "--relay-ipv6", + dest="relay_ipv6", + help="The ipv6 address of the relay you want to forward traffic to", + ) + parser.add_argument( + "--dry-run", + dest="dry_run", + action="store_true", + help="don't actually modify the server", + ) + + +def proxy_cmd(args, out): + """Deploy reverse proxy on a second server.""" + env = os.environ.copy() + env["RELAY_IPV4"] = args.relay_ipv4 + env["RELAY_IPV6"] = args.relay_ipv6 + deploy_path = importlib.resources.files(__package__).joinpath("proxy-deploy.py").resolve() + pyinf = "pyinfra --dry" if args.dry_run else "pyinfra" + + sshexec = args.get_sshexec() + # :todo make sure relay is deployed to args.relay_ipv4 and args.relay_ipv6 + + # abort if IP address == the chatmail relay itself: if port 22 is open AND /etc/chatmail-version exists + if sshexec.logged(call=remote.rshell.get_port_service, args=[22]): + if sshexec.logged(call=remote.rshell.chatmail_version): + out.red("Can not deploy proxy on the chatmail relay itself, use another server") + return 1 + cmd = f"{pyinf} --ssh-user root {args.ip_address} {deploy_path} -y" + out.check_call(cmd, env=env) # during first try, only set SSH port to 2222 + + cmd = f"{pyinf} --ssh-port 2222 --ssh-user root {args.ip_address} {deploy_path} -y" + try: + retcode = out.check_call(cmd, env=env) + if retcode == 0: + out.green("Reverse proxy deployed - you can distribute the IP address now.") + else: + out.red("Deploying reverse proxy failed") + except subprocess.CalledProcessError: + out.red("Deploying reverse proxy failed") + retcode = 1 + return retcode + + def fmt_cmd_options(parser): parser.add_argument( "--check", diff --git a/cmdeploy/src/cmdeploy/proxy-deploy.py b/cmdeploy/src/cmdeploy/proxy-deploy.py new file mode 100644 index 00000000..ffcb4de4 --- /dev/null +++ b/cmdeploy/src/cmdeploy/proxy-deploy.py @@ -0,0 +1,19 @@ +import os + +import pyinfra +from pyinfra import host + +from proxy import configure_ssh, configure_proxy + + +def main(): + ipv4_relay = os.getenv("IPV4_RELAY") + ipv6_relay = os.getenv("IPV6_RELAY") + + configure_ssh() + if host.data.get("ssh_port") not in (None, 22): + configure_proxy(ipv4_relay, ipv6_relay) + + +if pyinfra.is_cli: + main() diff --git a/cmdeploy/src/cmdeploy/proxy.py b/cmdeploy/src/cmdeploy/proxy.py new file mode 100644 index 00000000..0e8297ac --- /dev/null +++ b/cmdeploy/src/cmdeploy/proxy.py @@ -0,0 +1,63 @@ +import importlib + +from pyinfra import host +from pyinfra.operations import files, server, apt, systemd + +def configure_ssh(): + files.replace( + name="Configure sshd to use port 2222", + path="/etc/ssh/sshd_config", + text="Port 22\n", + replace="Port 2222\n", + ) + systemd.service( + name="apply SSH config", + service="ssh", + reloaded=True, + ) + apt.update() + + +def configure_proxy(ipv4_relay, ipv6_relay): + files.put( + name="Configure nftables", + src=importlib.resources.files(__package__).joinpath("proxy_files/nftables.conf.j2"), + dest="/etc/nftables.conf", + ipv4_address=ipv4_relay, # :todo what if only one of them is specified? + ipv6_address=ipv6_relay, + ) + + server.sysctl(name="enable IPv4 forwarding", key="net.ipv4.ip_forward", value=1, persist=True) + + server.sysctl( + name="enable IPv6 forwarding", + key="net.ipv6.conf.all.forwarding", + value=1, + persist=True, + ) + + server.shell( + name="apply forwarding configuration", + commands=[ + "sysctl -p", + "nft -f /etc/nftables.conf", + ], + ) + + if host.data.get("floating_ips"): + i = 0 + for floating_ip in host.data.get("floating_ips"): + i += 1 + files.template( + name="Add floating IPs", + src="servers/proxy-nine/files/60-floating.ip.cfg.j2", + dest=f"/etc/network/interfaces.d/{59 + i}-floating.ip.cfg", + ip_address=floating_ip, + i=i, + ) + + systemd.service( + name="apply floating IPs", + service="networking", + restarted=True, + ) diff --git a/cmdeploy/src/cmdeploy/proxy_files/60-floating.ip.cfg.j2 b/cmdeploy/src/cmdeploy/proxy_files/60-floating.ip.cfg.j2 new file mode 100644 index 00000000..7eae9ea2 --- /dev/null +++ b/cmdeploy/src/cmdeploy/proxy_files/60-floating.ip.cfg.j2 @@ -0,0 +1,4 @@ +auto eth0:{{ i }} +iface eth0:{{ i }} inet static + address {{ ip_address }} + netmask 32 diff --git a/cmdeploy/src/cmdeploy/proxy_files/nftables.conf.j2 b/cmdeploy/src/cmdeploy/proxy_files/nftables.conf.j2 new file mode 100644 index 00000000..82310a3f --- /dev/null +++ b/cmdeploy/src/cmdeploy/proxy_files/nftables.conf.j2 @@ -0,0 +1,67 @@ +#!/usr/sbin/nft -f + +flush ruleset + +define wan = eth0 + +# which ports to proxy +define ports = { smtp, http, https, imap, imaps, submission, submissions } + +# the host we want to proxy to +define ipv4_address = {{ ipv4_address }} +define ipv6_address = [{{ ipv6_address }}] + +table ip nat { + chain prerouting { + type nat hook prerouting priority dstnat; policy accept; + iif $wan tcp dport $ports dnat to $ipv4_address + } + + chain postrouting { + type nat hook postrouting priority 0; + + oifname $wan masquerade + } +} + +table ip6 nat { + chain prerouting { + type nat hook prerouting priority dstnat; policy accept; + iif $wan tcp dport $ports dnat to $ipv6_address + } + + chain postrouting { + type nat hook postrouting priority 0; + + oifname $wan masquerade + } +} + +table inet filter { + chain input { + type filter hook input priority filter; policy drop; + + # Accept ICMP. + # It is especially important to accept ICMPv6 ND messages, + # otherwise IPv6 connectivity breaks. + icmp type { echo-request } accept + icmpv6 type { echo-request, nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept + + # Allow incoming SSH connections. + tcp dport { 22, 2222 } accept + # Allow incoming shadowsocks connections. + tcp dport { 8388 } accept + + ct state established accept + } + chain forward { + type filter hook forward priority filter; policy drop; + + ct state established accept + ip daddr $ipv4_address counter accept + ip6 daddr $ipv6_address counter accept + } + chain output { + type filter hook output priority filter; + } +} diff --git a/cmdeploy/src/cmdeploy/remote/rshell.py b/cmdeploy/src/cmdeploy/remote/rshell.py index f8166816..fc7b0289 100644 --- a/cmdeploy/src/cmdeploy/remote/rshell.py +++ b/cmdeploy/src/cmdeploy/remote/rshell.py @@ -21,6 +21,20 @@ def shell(command, fail_ok=False, print=print): return "" +def get_port_service(port: int) -> str: + return shell( + "ss -lptn 'src :%d' | awk 'NR>1 {print $6,$7}' | sed 's/users:((\"//;s/\".*//'" + % (port,) + ) + + +def chatmail_version(): + version = shell("cat /etc/chatmail-version") + if "cat: /etc/chatmail-version:" in version: + version = None + return version + + def get_systemd_running(): lines = shell("systemctl --type=service --state=running").split("\n") return [line for line in lines if line.startswith(" ")]