Skip to content

External Traffic Flow & Redirects

Single Entry Point Design

All external traffic enters through a single WAN IP on BPI-R4 and is demultiplexed by protocol on the jump host. This approach means only one port is exposed externally for all TCP traffic (HTTPS, SSH, OpenVPN), reducing the attack surface.

flowchart TD
    client["External Client"]

    subgraph bpir4["BPI-R4 — OpenWrt 25.12"]
        dnat_tcp["TCP :443 DNAT\n→ mbptillo:4443"]
        dnat_udp["UDP :443 DNAT\n→ mbptillo:4443"]
    end

    subgraph mbptillo["mbptillo — 192.168.1.246"]
        sslh["sslh :4443\nprotocol demux"]
        socat["socat UDP4/UDP6-LISTEN:4443\n→ localhost:443"]
        mosh_srv["mosh-server :443\nauthbind"]
        openvpn["OpenVPN :9443\ntun10 10.8.10.0/24"]
        sshd["sshd :22"]
    end

    subgraph k8s["mdapi-prod"]
        ingress["ingress-nginx\n192.168.1.191:443"]
    end

    client -->|"TCP :443"| dnat_tcp --> sslh
    client -->|"UDP :443"| dnat_udp --> socat --> mosh_srv

    sslh -->|"TLS"| ingress
    sslh -->|"SSH"| sshd
    sslh -->|"OpenVPN"| openvpn

    ingress --> svc["*.mdapi.ch services"]

Protocol Demultiplexing with sslh

sslh inspects the first bytes of each TCP connection to identify the protocol, then forwards to the appropriate backend — all on the same port. This allows SSH, HTTPS, and OpenVPN to coexist on port 443, which is reliably unblocked on most networks (hotels, corporate firewalls).

Protocol detected Forwarded to
TLS / HTTPS ingress-nginx at 192.168.1.191:443
SSH local sshd at :22
OpenVPN OpenVPN server at 127.0.0.1:9443

mosh Relay

mosh uses UDP for its transport layer, which requires separate handling from the TCP sslh demux. Two socat instances forward UDP from the external port to the local mosh-server.

flowchart LR
    mc["mosh client\nvpn.home.tillo.ch:443"]
    bpir4["BPI-R4\nDNAT UDP :443 → :4443"]
    sc["socat UDP-LISTEN:4443,fork\n→ UDP:localhost:443"]
    ms["mosh-server :443\nauthbind"]

    mc --> bpir4 --> sc --> ms

socat LISTEN vs RECVFROM

socat UDP-LISTEN:4443,fork forks once per client session, preserving the source IP:port for the lifetime of the connection. Using UDP-RECVFROM:4443,fork instead forks per packet, creating a new ephemeral source port each time — mosh-server interprets each packet as a new client and the session breaks immediately.

TLS Certificate Automation

All certificates are issued by Let's Encrypt via cert-manager using DNS-01 challenges. DNS-01 is used instead of HTTP-01 because it supports wildcard certificates and works for services that are not publicly reachable.

flowchart LR
    ing["Ingress\ncert-manager.io/issuer:\nletsencrypt-prod"]
    cm["cert-manager"]
    bind9["BIND9\nauthoritative nameserver\n192.168.1.53"]
    le["Let's Encrypt\nACME v2"]
    secret["TLS Secret\nin namespace"]

    ing --> cm
    cm -->|"nsupdate RFC 2136\nTSIG signed"| bind9
    bind9 -->|"_acme-challenge TXT"| le
    le -->|"certificate"| cm --> secret --> ing

The BIND9 instance is the authoritative nameserver for mdapi.ch, tillo.ch, and all hosted zones. cert-manager uses TSIG to sign DNS update requests (nsupdate RFC 2136), so no DNS provider API token is needed — the key stays on-premise.

Ingress Routing

ingress-nginx terminates TLS and routes by hostname. ModSecurity WAF runs inline on every request.

flowchart LR
    nginx["ingress-nginx\n192.168.1.191\nModSecurity WAF"]

    nginx --> ha["home.mdapi.ch\nHome Assistant"]
    nginx --> joplin["notes.mdapi.ch\nJoplin"]
    nginx --> windmill["windmill.mdapi.ch\nWindmill"]
    nginx --> nextcloud["cloud.envuassu.ch\nNextcloud"]
    nginx --> keycloak["login.envuassu.ch\nKeycloak"]
    nginx --> more["...30+ more services"]

GitLab uses its own bundled nginx-ingress-controller (at 192.168.1.197) to handle its own domains (gitlab.mdapi.ch, registry.mdapi.ch, kas.mdapi.ch).