Wireguard auf einfach

Wireguard, dieses VPN kann einen ganz schön beschäftigen. Zunächst vermutet man die Funktionalität anders gelagert, ehe man später herausfindet, wie es ist und es korrekt macht. Um diesen Weg zu vereinfachen, habe ich mir gedacht, dass ich mal einige klärende Sätze los werde und meine paar Skripte dazu gebe, mit denen ich das Ganze Management ziemlich einfach gestaltet habe.

Wireguard das Wesen

Wireguard ist toll simpel und doch in der Konfiguration sehr komplex. Zumindest, wenn man unbedarft ran geht, versteht man erst mal nicht was eine gute Konfiguration ist und welche Randparameter einzuhalten sind.

Erkenntnis

Die Erkenntnisse dazu sind:

  • Wireguard ist eine Punkt-zu-Punkt VPN-Verbindung. Das heißt, es werden keine Netze verbunden sondern immer nur zwei Interfaces miteinander.
  • Daraus leitet sich die Folge ab, dass der Verkehr also über das einzurichtende Zwischennetz geroutet wird.
  • Ein Paket wandert also von einem Netz durch den Forwarder in das VPN-Netz und auf der anderen Seite in das ferne Netz, wieder durch den Forwarder des VPN-Routers.
  • Wireguard kann nur immer einen Gegner pro Interface und Port haben
  • Man muss also viele kleine Netze anlegen und jeweils eigene Ports nutzen
  • Es gibt Konfigs für Clients und für Server
  • Die Schlüssel darin sind anitsymmetrisch darin verteilt (perfekt zur Automatisierung)
  • Die IP-Adressen sind ebenfalls antisymmetrisch
  • Andere Dinge sind gleich oder nur hier oder dort anzuwenden

Anwendungsfall

Ich beschränke mich hier auf den Anwendungsfall Netzkopplung mit einem zentralen Router. Sprich es gibt mehrere Clients und diese bringen entweder sich selbst oder zusätzlich ein ganzes Netz herein. Ab da funktioniert das Routing auch zwischen den Netzen (Voraussetzung sind allerdings gepflegte statische Routen auf dem Standardgateway der gekoppelten Netze = war immer so).

Skripte

Dieses wurde berücksichtigt, um die folgenden Skripte zu erstellen. Als einzigstes Ding muss man sich einen Namen ausdenken für den neuen Client. Und natürlich muss die dabei entstandene Client-Konfig auf den Client verbracht werden.

Die Skripte sind dazu gedacht, im /etc/wireguard-Verzeichnis zu residieren und dort lokal Änderungen zu machen und als root systemctl aufzurufen.

Als Infrastruktur kommt ein _-Verzeihnis mit. Darin sind die Vorlagedateien für die Server- und Client-Konfig drin mit Platzhalter. Das mk_-client.sh-Skript macht dann mit sed einen Such- und Ersetzenlauf. Weiterhin sind in diesem Verzeichnis .txt-Dateien, die die jeweils zuletzt vergebenene IP/Port enthält. Diese Dateien sind bei Bedarf/zu Beginn zu pflegen.

_/last-ip.txt

10.254.0.4

Anpassen bei Bedarf – das 10er Netz scheint gut. Es wird immer 2 hochgezählt, da ja immer Point-to-Point zwei Adressen gebraucht werden.

_/last-port.txt
14264

Hier wird der letzte Port gemerkt und weiter hochgezählt. alles uter 64k ist gut.

_/client.conf
[Interface]
# set address to next address
Address = :CLIENT_IP:/32
PrivateKey = :CLIENT_KEY:
#DNS = 8.8.8.8

[Peer]
PublicKey = :SERVER_PUB_KEY:
Endpoint = :SERVER_ADDRESS:::PORT:
PresharedKey = :PSK:
# Route only vpn trafic through vpn
AllowedIPs = 10.254.0.0/24, 192.168.88.0/24, 192.168.22.0/24
# Route ALL traffic through vpn
#AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 21ds

Hier sind Platzhalter mit :PLH:-Notation drin, die beim Erzeugen ersetzt werden. Bei AllowedIPs kann der geneigte Admin all seine Netze hinzufügen. Da dieser Teil kopiert wird, müssen alle erstellten client.confs angepasst werden, wenn neue Netze hinzukommen. In diesem Fall sind es /24-Netze.

_/server.conf
[Interface]
Address = :SERVER_IP:/32
MTU = 1420
ListenPort = :PORT:
PrivateKey = :SERVER_KEY:
PostUp = /etc/wireguard/wg-iptables-updown.sh :IF_NAME: up
PostDown = /etc/wireguard/wg-iptables-updown.sh :IF_NAME: down

[Peer]
PublicKey = :CLIENT_PUB_KEY:
PresharedKey = :PSK:
AllowedIPs = :CLIENT_IP:/32

Dies ist die Vorlage für neue Server-Konfigs. Interessant dabei, dass die eigene und Gegen-IP des VPN-Netzes /32-Adressen sind. Also genau je eine Adresse. Zudem ist hier der wg-iptables-updown.sh – Aufruf drin, der das Routing auf dem zentralen Router aktualisiert und entsprechende Forwarding-Regeln einfügt oder entfernt. Diese Datei ist auch mit dabei. Siehe hier:

wg-iptables-updown.sh
#!/bin/sh

iptables="/usr/sbin/iptables"

if [ -z "$1" ]; then
	echo "No interface!"
	echo "Usage: $0 [interface] [action]"
	exit 0

fi

if [ -z "$2" ]; then
	echo "No action!"
	echo "Usage: $0 [interface] [action]"
	echo "Actions:"
	echo "* up"
	echo "* down"
	exit 0

elif [ "$2" = "up" ]; then
	action="-A"

elif [ "$2" = "down" ]; then
	action="-D"

else
	echo "Unknown action!"
	echo "Usage: $0 [interface] [action]"
	echo "Actions:"
	echo "* up"
	echo "* down"
	exit 0

fi

$iptables $action FORWARD -i $1 -j ACCEPT
$iptables $action FORWARD -o $1 -j ACCEPT

Hauptteil

Den Hauptteil bilden die zwei Skripte mk-client.sh und rm-client.sh

Damit wird ein neuer VPN-Entpunkt hinzugefügt bzw entfernt.

mk-client.sh

Einzig der Name für diese Verbindung wird als Parameter gebraucht. Es wird dafür ein öffentlicher und Privater Schlüssel und ein neues Geheimnis ausgewürfelt und in entsprechenden Dateien im ./clients/-Verzeichnis gespeichert. Von dort kann man die Dateien (eigentlich nur die .cofig) für den Client extrahieren und weitergeben. Die Server-.config wird im /etc/wireguard-Verzeichnis abgelegt und ist somit direkt verfügbar. Das wird auch gleich genutzt und wireguard damit konfiguriert. Sowohl die client- als auch die server-Konfig sind Kopien der Vorlagedateien. Die Platzhalter (wie z.B. Schlüssel und IPs) werden durch sed-Aufrufe ersetzt. So einfach.
Am Ende kommt noch eine Frage, ob man denn die Konfig gleich in systemd und beim Systemstart aktivieren möchte.

#!/bin/bash
VPN_HOST=vpn.flinkebits.de

if [ $# -eq 0 ]
then
	echo "must pass a client name as an arg: mk-client.sh new-client"
else
	umask 077
	echo "Creating client config for: $1"
	mkdir -p clients/$1
	wg genkey | tee clients/$1/$1.priv | wg pubkey > clients/$1/$1.pub
	CLIENT_KEY=$(cat clients/$1/$1.priv)
	CLIENT_PUB_KEY=$(cat clients/$1/$1.pub)
        infix=$(cat _/last-ip.txt | tr "." " " | awk '{print $4}')
	ips="10.254.0."$(expr $infix + 1)
	ipc="10.254.0."$(expr $infix + 2)
        lastport=$(cat _/last-port.txt)
        port=$(expr $lastport + 1)
	wg genpsk > clients/$1/$1.psk
	PSK=$(cat clients/$1/$1.psk)

	wg genkey | tee clients/$1/server.priv | wg pubkey > clients/$1/server.pub
        SERVER_KEY=$(cat clients/$1/server.priv)
        SERVER_PUB_KEY=$(cat clients/$1/server.pub)


  cat _/server.conf | sed -e 's|:PSK:|'"$PSK"'|' | sed -e 's/:SERVER_IP:/'"$ips"'/' | sed -e 's/:CLIENT_IP:/'"$ipc"'/' | sed -e 's|:SERVER_KEY:|'"$SERVER_KEY"'|' | sed -e 's|:CLIENT_PUB_KEY:|'"$CLIENT_PUB_KEY"'|' | sed -e 's|:PORT:|'"$port"'|' | sed -e 's|:IF_NAME:|'"wg-$1"'|' > wg-$1.conf

  cat _/client.conf | sed -e 's|:PSK:|'"$PSK"'|' | sed -e 's/:CLIENT_IP:/'"$ipc"'/' | sed -e 's|:CLIENT_KEY:|'"$CLIENT_KEY"'|' | sed -e 's|:SERVER_PUB_KEY:|'"$SERVER_PUB_KEY"'|' | sed -e 's|:PORT:|'"$port"'|' | sed -e 's|:SERVER_ADDRESS:|'"$VPN_HOST"'|' > clients/$1/$1.conf

	echo "Erzeuge in clients/$1 $1.priv, $1.pub, server.priv, server.pub"
	echo "Erzeuge clients/$1/$1.conf"
	echo "Erzeuge wg-$1.conf"
	echo "Speichere zuletzt verwendete IP, Port: $ipc : $port"
	echo $ipc > _/last-ip.txt
	echo $port > _/last-port.txt
	echo "Konfig fertig!"

	read -p "Aktivieren von $1 in systemctl? (y/n) " yn

	case $yn in 
		[yY] ) echo ok, we will proceed;
			systemctl enable wg-quick@wg-$1.service
			systemctl start wg-quick@wg-$1
			;;
		* ) echo exiting...;
		exit;;
	esac
fi

rm-client.sh

Die rm-client macht es recht einfach. Fährt das interface ordentlich runter, entfernt es aus systemd und löscht die Dateien:

#!/bin/bash

if [ $# -eq 0 ]
then
        echo "must pass a client name as an arg: $0 aclient"
else
	wg-quick down wg-$1
	systemctl stop wg-quick@wg-$1
	systemctl disable wg-quick@wg-$1.service
	rm -rfv "/etc/wireguard/clients/$1/"
	rm -v "/etc/wireguard/wg-$1.conf"
fi

GIT-Repo

Das Ganze könnt ihr auch in einem Git-Repo auf einmal herunterladen und in euer /etc/wireguard-Vz werfen. https://github.com/ChaosChemnitz/Wireguard-einfach

Lodgify per API abfragen

Seit einiger Zeit bin ich mit meiner Familie dabei, eine Ferienwohnung zu betreiben. Das ist eher aus der Not heraus geworden, denn die Gewerbeeinheit ließ sich sonst nicht vermieten. Ergo musste man halt mal selbst ran an die Sache.

Ich will hier gar nicht abschweifen in die Untiefen der Ferienwohnungen und deren Implikationen. Nur so viel: Ohne Buchungsportale geht heute quasi nix und wer nicht mindestens auf AirBnB und booking unterwegs ist, bekommt nichts vom Kuchen ab. Doch wie verwaltet man die mindestens zwei „Kanäle“? Man braucht einen „Channelmanager“. Davon gibt es in der echt recht korrupten Hoteleriebranche viele. Sie überbieten sich meist eher darin, ein möglichst großes Stück von deinem verdienten Geld mit abzugreifen – mit Prozenten etc.

Am Ende entscheidet man sich für irgendwas, was mit halbwegs normalen Kosten und Features wie dynamische Preise und (sowieso) Synchronisation der Kalender aufwartet. Bei uns ist das, wie auch immer, Lodgify geworden.

Aber jetzt kommts: Die bieten zwar einen Basistarif (der für uns die richtige Mischung ist), aber die echten Basisfeatures bieten sie halt da nicht an. Edit: Wundere mich gerade, warum überhaupt Kanäle buchbar angebunden sind??! Also was fehlt: Es gibt nicht mal eine absolute Basisübersicht über die Buchungen des aktuellen Monats und die Einnahmen. Dummerweise sollte man diese Daten haben, wenn man die lokalen Steuern bezahlen will. Aber dieses Feature gibt es auch nicht im Pro-Plan, sondern nur im Ultra-Plan. OKOK, ist wohl absolut ultra, dass ich mal so eine Übersicht als Tabelle benötige… nun ja.

Zum Glück aber gibt es ein Integrations-API. Damit konnte ich mit doch sehr überschaubarem Aufwand meine Buchungen abrufen. Hier mal mein Vorschlag, das in Python zu machen. Viel Spaß beim nachmachen:

Pythonimplementierung:

import requests
import pandas as pd
from datetime import datetime

# ggf installieren:
# pip install requests pandas openpyxl

# API-Konfiguration
API_KEY = "<bitte füllen>"  # Ersetze mit deinem Lodgify API-Key
BASE_URL = "https://api.lodgify.com/v2/reservations/bookings"

# Zeitrahmen für die Abfrage (optional)
params = {
    "size":100,
    "includeExternal": True,
    "stayFilter": "Historic",
    "trash": False
}

# API-Anfrage mit korrektem X-ApiKey-Header
headers = {
    "X-ApiKey": API_KEY,
    "Accept-Language": "de",
    "accept": "application/json"
}

try:
    print("Starte API-Anfrage...")
    response = requests.get(BASE_URL, headers=headers, params=params, timeout=10)
    response.raise_for_status()  # Löst HTTPError für 4xx/5xx Statuscodes aus
    data = response.json()

    # Prüfe, ob die Antwort die erwartete Struktur hat
    if not isinstance(data, dict) or "items" not in data:
        raise ValueError("Ungültige API-Antwort: JSON element 'items' nicht gefunden.")

except requests.exceptions.HTTPError as errh:
    print(f"HTTP-Fehler: {errh}")
    print(f"Statuscode: {response.status_code}")
    print(f"Antwort: {response.text}")
    exit(1)
except requests.exceptions.ConnectionError as errc:
    print(f"Verbindungsfehler: {errc}")
    exit(1)
except requests.exceptions.Timeout as errt:
    print(f"Timeout-Fehler: {errt}")
    exit(1)
except requests.exceptions.RequestException as err:
    print(f"Anfragefehler: {err}")
    exit(1)
except ValueError as ve:
    print(f"Datenfehler: {ve}")
    exit(1)

# Daten aufbereiten
bookings = []
for booking in data.get("items", []):
    arrival = booking.get("arrival")
    departure = booking.get("departure")
    rooms = booking.get("rooms", [])
    total_amount = booking.get("total_amount", 0)
    guest = booking.get("guest")

    # Anzahl der Tage berechnen
    if arrival and departure:
        try:
            arrival_date = datetime.strptime(arrival, "%Y-%m-%d")
            departure_date = datetime.strptime(departure, "%Y-%m-%d")
            num_days = (departure_date - arrival_date).days
        except ValueError:
            num_days = 0
    else:
        num_days = 0

    # Gast-Informationen aus dem ersten Zimmer extrahieren
    adults = 0
    children = 0
    people = 0
    if rooms:
        guest_breakdown = rooms[0].get("guest_breakdown", {})
        adults = guest_breakdown.get("adults", 0)
        children = guest_breakdown.get("children", 0)
        people = rooms[0].get("people", 0)

    bookings.append({
        "Buchungs-ID": booking.get("id"),
        "Gastname": guest.get("name"),
        "Startdatum": arrival,
        "Enddatum": departure,
        "Anzahl Tage": num_days,
        "Anzahl Erwachsene": adults,
        "Anzahl Kinder": children,
        "Anzahl Personen": people,
        "Einnahmen (€)": total_amount
    })


# DataFrame erstellen
df = pd.DataFrame(bookings)

# In Excel-Datei speichern
output_file = "lodgify_buchungen.xlsx"
try:
    df.to_excel(output_file, index=False, engine="openpyxl")
    print(f"✅ Daten wurden erfolgreich in {output_file} gespeichert.")
except Exception as e:
    print(f"❌ Fehler beim Speichern der Excel-Datei: {e}")
    exit(1)

Ergebnis ist eie XLS-Datei mit den Buchungen und Einnahmen