RS232-Geräte über Netzwerk sicher verbinden

Dieses Projekt befindet sich noch in der Entwicklungsphase. Die Angaben hier stellen nur den aktuellen Zwischenstand dar und können sich deshalb fortlaufend ändern.

Aufgabenstellung

Zu einer Amateuerfunkanlage soll eine sichere Verbindung zur Steuerung des Antennenrotors aufgebaut werden. Die serielle RS232-Schnittstelle des Antennenrotors soll dabei remote über TCP angesprochen werden.

Softwarekomponenten

Mittels der Software Tailscale kann eine Verbindung zwischen zwei Geräten vermittelt werden. Damit kein externer Cloud-Server von Tailscale verwendet werden muss, läuft auf einem Raspberry Pi, der mit einer festen öffentlichen IP-Adresse ausgestattet ist, die Software headscale in Form eines Docker-Containers. Diese Software dient also als „Vermittlungsstelle“ für die Tailscale-Software auf den beteiligten Clients. Nachdem diese von Headscale die IP-Adressen der Gegenstelle erhalten haben, kommunizieren die Geräte direkt miteinander. Der Headscale-Server ist an dieser Kommunikation nicht mehr beteiligt.

Die serielle Schnittstelle wird durch die Software ser2net mit dem TCP-Netwerk verbunden und mit socat werden die Daten zwischen zwei IP-Adressen transportiert.

Übersicht der Topologie

[Internet]

▼ (91.137.72.107)
[Haupt-Raspberry Pi]

├── [Traefik (Netzwerk: proxy)] ────┬──── [Headscale-Container] (Port 8080, Domain: headscale.lang-dieter.de)
│ │
│ ▼ (Tailscale-VPN)
│ [Externer Raspberry Pi]
│ │
│ ▼
│ [socat/ser2net] ──── [Funkstation (RS232)]

└── [Andere Container]

[Anwender mit Client-Rechner Windows]

├── [SPID-Softwar Rotorsteuerung]
│ │
│ ▼ (Tailscale-VPN)

Erweiterung am bestehenden Traefik-Server mit öffentlicher IP

headscale als Container einrichten

Headscale wird auf dem bestehenden Rasperry Pi mit Traefik im Hintergrund aus zusätzlicher Container hinzugefügt.

Ich verwende folgende Verzeichnisstruktur:
/srv/containers/
├── traefik/ # Bestehende Traefik-Instanz (Netzwerk: proxy)
│ └── docker-compose.yml

├── headscale/ # Headscale-Instanz
│ ├── docker-compose.yml
│ ├── config/
│ │ ├── config.yaml
│ │ └── acls.yaml
│ └── data/

└── … # Andere Container

Inhalt der Datei /src/containers/headscale/docker-compose.yml:

services:
  headscale:
    image: headscale/headscale:latest
    container_name: headscale
    restart: unless-stopped
    read_only: true
    tmpfs:
      - /var/run/headscale
    ports:
      - target: 3478
        published: 3478
        protocol: udp
        mode: host
    volumes:
      - ./config:/etc/headscale:ro
      - ./data:/var/lib/headscale
    command: serve
    healthcheck:
      test: ["CMD", "headscale", "health"]
    networks:
      - proxy
    labels:
      - traefik.enable=true

      # HTTP-Router
      - traefik.http.routers.headscale-http.rule=Host(`headscale.lang-dieter.de`)
      - traefik.http.routers.headscale-http.entrypoints=web

      # HTTPS-Router
      - traefik.http.routers.headscale-https.rule=Host(`headscale.lang-dieter.de`)
      - traefik.http.routers.headscale-https.entrypoints=websecure
      - traefik.http.routers.headscale-https.tls=true
      - traefik.http.routers.headscale-https.tls.certresolver=tls_resolver
      - traefik.http.routers.headscale-https.tls.domains[0].main=headscale.lang-dieter.de

      # Middleware für DERP Websockets
      - traefik.http.routers.headscale-https.middlewares=headscale-headers
      - traefik.http.middlewares.headscale-headers.headers.customrequestheaders.Connection=Upgrade
      - traefik.http.middlewares.headscale-headers.headers.customrequestheaders.Upgrade=websocket

      # Service-Konfiguration
      - traefik.http.services.headscale.loadbalancer.server.scheme=http
      - traefik.http.services.headscale.loadbalancer.server.port=8087

networks:
  proxy:
    external: true


Die Konfiguration wird in der Datei /srv/containers/headscale/config/config.yaml hinterlegt:

# =============================================================================
# HEADSCALE KONFIGURATION
# Angepasst für Traefik Reverse Proxy & Subdomain headscale.lang-dieter.de
# =============================================================================

# Die externe URL unter der Traefik Ihr Headscale erreichbar macht
server_url: https://lang-dieter.de

# Adressen, auf denen Headscale INNERHALB des Docker-Containers lauscht
listen_addr: 0.0.0.0:8087
metrics_listen_addr: 0.0.0.0:9097

# =============================================================================
# DATENBANK (SQLite)
# =============================================================================
database:
  type: sqlite
  sqlite:
    path: /var/lib/headscale/db.sqlite

# =============================================================================
# NETZWERK-PRÄFIXE (Interne VPN-IPs für Ihre Geräte)
# =============================================================================
prefixes:
  v4: 100.64.0.0/10
  v6: fd7a:115c:a1e0::/48

# =============================================================================
# DNS EINSTELLUNGEN
# =============================================================================
dns:
  magic_dns: false
  base_domain: net.lang-dieter.de
  override_local_dns: true
  nameservers:
    global:
      - 1.1.1.1
      - 8.8.8.8
  search_domains: []
  extra_records: []

# =============================================================================
# KRYPTOGRAFIE SCHLÜSSEL
# =============================================================================
private_key_path: /var/lib/headscale/private.key
noise:
  private_key_path: /var/lib/headscale/noise_private.key

# =============================================================================
# DERP (RELAY SERVER) EINSTELLUNGEN
# =============================================================================
derp:
  # Keine externen, öffentlichen Tailscale-Server abfragen
  urls: []  
  paths: []
  auto_update_enabled: false

  # Integrierten DERP-Server aktivieren
  server:
    enabled: true
    region_id: 901
    region_code: "raspi-relay"
    region_name: "Mein privater Raspi DERP Server"
    
    # WICHTIG: Auf 8088 geändert, um Konflikt mit dem Hauptdienst (8087) zu vermeiden!
    listen_addr: "0.0.0.0:8088"  
    
    # STUN läuft über UDP und wird in der Compose direkt nach außen gereicht
    stun_listen_addr: "0.0.0.0:3478"
    private_key_path: /var/lib/headscale/derp_server_private.key

Nun starten wir den Container mit:

docker compose up -d &

Firewall-Regeln am Host ergänzen

sudo ufw allow 3478/udp   # STUN für DERP
sudo ufw enable

Headscale-ACLs (Zugangskontrolle einrichten)

sudo nano /srv/containers/headscale/config/acls.yaml
acls:
  - action: accept
    src: ["100.64.0.0/10"]  # Erlaube alle Headscale-IPs
    dst: ["100.64.0.0/10:5000"]  # Erlaube Zugriff auf Port 5000

Headscale-Benutzer für den Bereich Schlüsselverwaltung anlegen

docker exec -it headscale headscale user create funkstation-admin

Liste der Headscale-Benutzer
anzeigen lassen, da im nächsten Befehl die User-ID benötigt wird (in unserem Fall die Nummer 1)

docker exec -it headscale headscale user list

Auth-Key für den externen Raspberry Pi (an der Funkstation) erzeugen:

docker exec -it headscale headscale preauthkey create --user 1 --expiration=24h --reusable

Den damit erzeugten Key kopieren wir uns, damit er auf dem externen Raspi eingefügt werden kann.

Arbeiten auf externen Rasperry Pi an der Funkstation

Verbindung mit headscale einrichten

Tailscale installieren und den Geräte-Key hinterlegen (ersetze <DEIN_AUTH_KEY> durch den generierten Key):

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --login-server=https://headscale.lang-dieter.de --auth-key=<DEIN_AUTH_KEY> --hostname=funkstation-pi

Wenn alles funktioniert, sollte folgender Befehl eine IP-Adresse im Bereich 100.64.x.y anzeigen:

tailscale status

TCP-to-Serial-Bridge am Funkgeräte-Pi auf Port 5000 zur seriellen Schnittstelle einrichten:

socat TCP-LISTEN:5000,fork /dev/ttyUSB0,raw,echo=0,baud=9600

Für Dauerbetrieb richten wir es als systemd-Dienst ein:

sudo nano /etc/systemd/system/tcp-serial-bridge.service

mit folgendem Inhalt:

[Unit]
Description=TCP to Serial Bridge for Funkstation
After=network.target

[Service]
ExecStart=/usr/bin/socat TCP-LISTEN:5000,fork /dev/ttyUSB0,raw,echo=0,baud=9600
Restart=always
User=root

[Install]
WantedBy=multi-user.target

Liste der Dienste aktualisieren und den neuen Dienst starten:

sudo systemctl daemon-reload
sudo systemctl enable tcp-serial-bridge
sudo systemctl start tcp-serial-bridge

Firewall-Regeln am Funkgeräte-Rechner ergänzen

sudo ufw allow 5000/tcp   # TCP-Port für Funkstation
sudo ufw enable