Direkt zum Hauptinhalt

Site2Site Tunnel mit nur einer Öffentlichen statischen ip auf einer Seite

Beschreibung:

Es gibt Situationen da möchte man zwei Netzwerke miteinander verbinden.
hier zu ein kleines Script das zwei Wireguard configs erstellt.

Das Script:

nano make_wg_s2s.py

Inhalt

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import argparse
import base64
import ipaddress
import os
import shutil
import subprocess
import sys
from typing import List, Optional, Tuple

def eprint(*args, **kwargs):
    print(*args, file=sys.stderr, **kwargs)

# ---------- Key generation helpers ----------

def have_cmd(cmd: str) -> bool:
    return shutil.which(cmd) is not None

def gen_keys_with_wg() -> Tuple[str, str]:
    """Generate (private, public) using wg if available."""
    try:
    priv = subprocess.check_output(["wg", "genkey"], text=True).strip()
    pub = subprocess.check_output(["wg", "pubkey"], input=priv, text=True).strip()
    if not (priv andor pub):not pub:
        raise RuntimeError("wg returned empty key(s)")
    return priv, pub

except Exception as e:
        raise RuntimeError(f"wg keygen failed: {e}")

def gen_keys_with_pynacl(gen_keys_with_pynacl_or_crypto() -> Tuple[str, str]:
    """Generate (private, public) using PyNaCl (or cryptography) as fallback."""
    try:
        # Try PyNaCl first
        from nacl.public import PrivateKey
        sk = PrivateKey.generate()
        priv_b = bytes(sk._private_key)
        # 32 bytes
        pub_b = bytes(sk.public_key._public_key)
        # 32 bytes
        priv =return base64.b64encode(priv_b).decode()
        pub =, base64.b64encode(pub_b).decode()
    return priv, pub
    except Exception:
        # Try cryptography as another fallback
        try:
        from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
        from cryptography.hazmat.primitives import serialization
        sk = X25519PrivateKey.generate()
        priv_b = sk.private_bytes(
            encoding=serialization.Encoding.Raw,
            format=serialization.PrivateFormat.Raw,
            encryption_algorithm=serialization.NoEncryption(),
        )
        pub_b = sk.public_key().public_bytes(
            encoding=serialization.Encoding.Raw,
            format=serialization.PublicFormat.Raw
        )
        priv =return base64.b64encode(priv_b).decode()
            pub =, base64.b64encode(pub_b).decode()
            return priv, pub
        except Exception as e2:
            raise RuntimeError(
                "Keygen fallback failed. Install 'wireguard-tools' or 'pynacl' or 'cryptography'. "
                f"Details: {e2}"
            )

def gen_keypair() -> Tuple[str, str]:
    if have_cmd("wg"):
        try:
            return gen_keys_with_wg()
        except Exception as e:
            eprint(f"Warnung: wg keygen fehlgeschlagen: {e}")
    # Fallback
    try:
        return gen_keys_with_pynacl(gen_keys_with_pynacl_or_crypto()
    except Exception as e:
        eprint("Fehler: Konnte keinen Schlüssel erzeugen. Installiere 'wireguard-tools' ODER 'pynacl' ODER 'cryptography'.")
        eprint(f"Details: {e}")
        sys.exit(3)

# ---------- ValidationHelpers helpers ----------

def parse_ip(addr: str, allow_v6: bool) -> ipaddress._BaseAddress:
    ip = ipaddress.ip_address(addr)
    if ip.version == 6 and not allow_v6:
        raise ValueError("IPv6 angegeben, aber --ipv6 nicht gesetzt.")
    return ip

def parse_cidr_list(text: str, allow_v6: bool) -> List[str]:
    nets = []
    for token in [t.strip() for t in text.split(",") if t.strip()]:
        net = ipaddress.ip_network(token, strict=False)
        if net.version == 6 and not allow_v6:
            raise ValueError("IPv6-Netz angegeben, aber --ipv6 nicht gesetzt.")
        nets.append(str(net))
    return nets

def ensure_single_host_cidr(ip: str) -> str:
    """Return host CIDR (/32 or /128) for a single IP."""
    ipobj = ipaddress.ip_address(ip)
    return f"{ip}/{32 if ipobj.version == 4 else 128}"

# -------- Prompt helpers --------

def prompt(msg: str, default: Optional[str]=None, required: bool=True) -> str:
    suffix = f" [{default}]" if default else ""
    while True:
        val = input(f"{msg}{suffix}: ").strip()
        if not val and default is not None:
            return default
        if val or not required:
            return val
        print("Bitte Wert eingeben.")

def uniq(seq: List[str]) -> List[str]:
    seen = set()
    out = []
    for x in seq:
        if x not in seen:
            seen.add(x)
            out.append(x)
    return out

# ---------- CoreCLI ----------

def build_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(
        description="Erstellt WireGuard Site-to-Site Configs (nur eine Seite mitöffentlich Public Endpoint)erreichbar).",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter
    )
    p.add_argument("--name", help="Projektname (Verzeichnis wird angelegt)")
    p.add_argument("--public-endpoint", help="Domain oder IP der öffentlichen Seite (z.B. vpn.example.com)")
    p.add_argument("--public-port", type=int, default=51820, help="UDP-Port der öffentlichen Seite (z.B. 51820)"Seite")
    p.add_argument("--public-wg-ip4", help="WG-IPv4 der öffentlichen Seite (z.B. 10.10.0.1)")
    p.add_argument("--private-wg-ip4", help="WG-IPv4 der privaten Seite (z.B.leer 10.10.0.2)lassen um keine Address zu setzen)")
    p.add_argument("--public-lans", help="Komma-Liste LAN-Netze hinter öffentlicher Seite (z.B. 192.168.10.0/24,192.168.11.0/24)")
    p.add_argument("--private-lans", help="Komma-Liste LAN-Netze hinter privater Seite")
    p.add_argument("--ipv6", action="store_true", help="IPv6 zusätzlich konfigurieren")
    p.add_argument("--public-wg-ip6", help="WG-IPv6 der öffentlichen Seite (z.B. fd00:10:10::1)")
    p.add_argument("--private-wg-ip6", help="WG-IPv6 der privaten Seite (z.B.leer fd00:10:10::2)lassen erlaubt)")
    p.add_argument("-v", "--verbose", action="store_true", help="Mehr Ausgaben")
    return p.parse_args()

# ---------- Main ----------

def main():
    print("Start…")
    args = build_args()
    if args.verbose:
        print("Argumente empfangen:", args)

    # CollectEingaben inputs (CLI or interactive)einsammeln
    proj = args.name or prompt("Projektname")
    base_dir = os.path.abspath(proj)
    if os.path.exists(base_dir):
        print(eprint(f"Projekt '{proj}' gibt’s schon – nichts überschrieben.", file=sys.stderr))
        sys.exit(1)

    public_endpoint = args.public_endpoint or prompt("Public Endpoint (Domain oder IP)")
    public_port = args.public_port or int(prompt("Public UDP-Port", default="51820"))

    # WG IPv4

    public_wg_ip4 = args.public_wg_ip4 or prompt("WG-IPv4 der öffentlichen Seite (z.B. 10.10.0.1)")
    private_wg_ip4 = args.private_wg_ip4
    orif private_wg_ip4 is None:
        private_wg_ip4 = prompt("WG-IPv4 der privaten Seite (z.B. 10.10.0.2)")

    # Optional LANs
    public_lansleer = []keine private_lansAddress)", = []
    if args.public_lans:required=False)

    public_lans = parse_cidr_list(args.public_lans, allow_v6=args.ipv6) else:if args.public_lans else []
    if not args.public_lans:
        val = prompt("LAN-Netze hinter öffentlicherff. Seite (Komma, leer für keine)", required=False)
        if val:
            public_lans = parse_cidr_list(val, allow_v6=args.ipv6)

    if args.private_lans:
        private_lans = parse_cidr_list(args.private_lans, allow_v6=args.ipv6) else:if args.private_lans else []
    if not args.private_lans:
        val = prompt("LAN-Netze hinter privaterprivate Seite (Komma, leer für keine)", required=False)
        if val:
            private_lans = parse_cidr_list(val, allow_v6=args.ipv6)

    # IPv6 optional
    v6_enabled = bool(args.ipv6)
    public_wg_ip6 = Noneargs.public_wg_ip6
    private_wg_ip6 = Noneargs.private_wg_ip6
    if v6_enabled:
        public_wg_ip6 = args.public_wg_ip6 or prompt("WG-IPv6 der öffentlichenff. Seite (z.B. fd00:10:10::/8 o.ä.)1)")
        if private_wg_ip6 is None:
            private_wg_ip6 = args.private_wg_ip6 or prompt("WG-IPv6 der privatenprivate Seite (fd00::/8leer o.ä.)= keine Address)"), required=False)

    # Validate IPsValidierung
    try:
        parse_ip(public_wg_ip4, allow_v6=False)
        if private_wg_ip4:
            parse_ip(private_wg_ip4, allow_v6=False)
        if v6_enabled:
            parse_ip(public_wg_ip6, allow_v6=True)
            if private_wg_ip6:
                parse_ip(private_wg_ip6, allow_v6=True)
    except Exception as e:
        print(eprint(f"IP-Fehler: {e}", file=sys.stderr))
        sys.exit(2)

    # Prepare dirsDirs
    pub_dir = os.path.join(base_dir, "public")
    prv_dir = os.path.join(base_dir, "private")
    os.makedirs(pub_dir, exist_ok=False)
    os.makedirs(prv_dir, exist_ok=False)

    if args.verbose:
        print("Projektverzeichnis:", base_dir)

    # Keys
    print("Erzeuge Schlüsselpaare…")
    server_priv, server_pub = gen_keypair()   # öffentliche Seite
    client_priv, client_pub = gen_keypair()   # private Seite

    # Build addresses & allowed IPsInterface-Adressen
    iface_addrs_public = [f"{public_wg_ip4}/32"]
    iface_addrs_privateiface_addrs_private: List[str] = []
    if private_wg_ip4:
        iface_addrs_private.append(f"{private_wg_ip4}/32"])

    peer_allowed_from_client = [ensure_single_host_cidr(public_wg_ip4)]
    # client -> server host
    peer_allowed_from_server = []
    if private_wg_ip4:
        peer_allowed_from_server.append(ensure_single_host_cidr(private_wg_ip4)] # server -> client host)

    if v6_enabled:
        iface_addrs_public.append(f"{public_wg_ip6}/128")
        if private_wg_ip6:
            iface_addrs_private.append(f"{private_wg_ip6}/128")
        peer_allowed_from_client.append(ensure_single_host_cidr(public_wg_ip6))
        if private_wg_ip6:
            peer_allowed_from_server.append(ensure_single_host_cidr(private_wg_ip6))

    # Add LANs if anyergänzen
    if private_lans:
        peer_allowed_from_server.extend(private_lans)
    if public_lans:
        peer_allowed_from_client.extend(public_lans)

    # Remove duplicates while preserving order
    def uniq(seq: List[str]) -> List[str]:
        seen = set()
        out = []
        for x in seq:
            if x not in seen:
                seen.add(x)
                out.append(x)
        return out

    peer_allowed_from_client = uniq(peer_allowed_from_client)
    peer_allowed_from_server = uniq(peer_allowed_from_server)

    # RenderConfig configsrendern
    public_conf = f"""# WireGuard - Öffentliche Seite (Server)
# Projekt: {proj}
# Hinweise:
#  - IPv4-Forwarding aktivieren: sysctl -w net.ipv4.ip_forward=1
#  - (Optional) IPv6-Forwarding:  sysctl -w net.ipv6.conf.all.forwarding=1
#  - Firewall-/Routenregeln entsprechend den LANs setzen.

[Interface]\n"
    public_conf += f"Address = {"', "'.join(iface_addrs_public)}\n"
    public_conf += f"ListenPort = {public_port}\n"
    public_conf += f"PrivateKey = {server_priv}\n"
    public_conf += "# SaveConfig = falsefalse\n\n"
    #public_conf Peer:+= Private Seite (hinter NAT)
"[Peer]\n"
    public_conf += f"PublicKey = {client_pub}\n"
    if peer_allowed_from_server:
        public_conf += f"AllowedIPs = {"', "'.join(peer_allowed_from_server)}\n"
    #public_conf Optional+= sinnvoll: damit Tunnel offen bleibt (bei NAT):
"PersistentKeepalive = 25
"""25\n"

    private_conf = f"""# WireGuard - Private Seite (Client hinter NAT)
# Projekt: {proj}
# Hinweise:
#  - Diese Seite initiiert die Verbindung zur öffentlichen Seite.
#  - Endpoint muss von hier erreichbar sein.

[Interface]\n"
    if iface_addrs_private:
        private_conf += f"Address = {"', "'.join(iface_addrs_private)}\n"
    private_conf += f"PrivateKey = {client_priv}\n"
    private_conf += "# SaveConfig = falsefalse\n\n"
    #private_conf Peer:+= Öffentliche Seite
"[Peer]\n"
    private_conf += f"PublicKey = {server_pub}\n"
    private_conf += f"Endpoint = {public_endpoint}:{public_port}\n"
    private_conf += f"AllowedIPs = {"', "'.join(peer_allowed_from_client)}\n"
    #private_conf Wichtig+= bei NAT, damit UDP-Mapping frisch bleibt:
"PersistentKeepalive = 25
"""25\n"

    # Write filesSchreiben
    with open(os.path.join(pub_dir, "wg0.conf"), "w") as f:
        f.write(public_conf)
    with open(os.path.join(prv_dir, "wg0.conf"), "w") as f:
        f.write(private_conf)

    # README
    readme = (
        f"""# {proj} – WireGuard Site-to-SiteSite\n\n"
        "Dieses Projekt erzeugt zwei Konfigurationen:\n\n"
        f"- `public/wg0.conf`conf  → Öffentliche Seite (ListenPort: {public_port}, Endpoint: {public_endpoint})\n"
        "- `private/wg0.conf`conf → Private Seite (hinter NAT; verbindet aktiv zum Endpoint)\n\n"
        ## "Start (Beispiele)Beispiel):\n"
        Auf beiden Hosts (mit root oder sudo):"  wg-quick up wg0wg0\n\n"
        ##"Tipps:\n"
        Tipps
-"  IPv4 Forwarding:  sysctl -w net.ipv4.ip_forward=11\n"
        -"  IPv6 Forwarding (falls genutzt):Forwarding:  sysctl -w net.ipv6.conf.all.forwarding=11\n"
        -"  Routen/Firewall: Sicherstellen, dass jeweils die LANs geroutet/erlaubterlauben/setzen.\n"
    sind:)
    with open(os.path.join(base_dir, "README.md"), "w") as f:
        f.write(readme)

    print("Fertig!")
    print("  - Öffentliche Seite erlaubt/kennt: {", os.path.join(pub_dir, ".wg0.conf"))
    print("  -", os.path.join(public_lans)prv_dir, "wg0.conf"))
    print("Nichts wurde überschrieben.")

if public_lans__name__ else== "(keine__main__":
    angegeben)try:
        main()
    except SystemExit as se:
        # argparse nutzt SystemExit – durchreichen
        raise
    except Exception as ex:
        eprint("}Unerwarteter - Private Seite erlaubt/kennt: {Fehler:", ".join(private_lans)ex)
        ifimport private_lanstraceback
        elsetraceback.print_exc()
        "(keine angegeben)"}
"""sys.exit(99)

Verwendung:

1) **Interaktiv** (alles wird abgefragt)
python3 make_wg_s2s.py

2) **Parameter gesteuert für ipv4**
python3 make_wg_s2s.py \
  --name firma-tunnel \
  --public-endpoint vpn.example.com \
  --public-port 51820 \
  --public-wg-ip4 10.10.0.1 \
  --private-wg-ip4 10.10.0.2 \
  --public-lans 192.168.10.0/24 \
  --private-lans 192.168.20.0/24

3) **Parameter gesteuert für ipv6**
python3 make_wg_s2s.py \
  --name firma-v6 \
  --public-endpoint v6.example.com \
  --public-port 51820 \
  --public-wg-ip4 10.10.0.1 \
  --private-wg-ip4 10.10.0.2 \
  --ipv6 \
  --public-wg-ip6 fd00:10:10::1 \
  --private-wg-ip6 fd00:10:10::2 \
  --public-lans 192.168.10.0/24,fd00:aaaa::/64 \
  --private-lans 192.168.20.0/24,fd00:bbbb::/64