From 39c59fd3ef2f0c0015e0e17767f8f2b5facadad3 Mon Sep 17 00:00:00 2001
From: Dmitrii Iurco
Date: Sat, 6 Jun 2026 04:51:38 -0400
Subject: [PATCH] feat: WireGuard endpoint override + fix Docker network label
issue
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Endpoint override:
- Add PUT /api/wireguard/endpoint to set endpoint_override in identity
config; GET returns detected, override, and effective endpoints
- _effective_endpoint() helper applies override in peer config generation
(wireguard.py and peer_dashboard.py); detected IP still shown in UI
- Add Endpoint Override input in WireGuard page — solves the common case
where auto-detected IP is a gateway/VPS but peers connect via LAN IP
Docker cell-network fix:
- Declare cell-network external in docker-compose.yml; Docker Compose v5
enforces label ownership and rejects networks created by older versions
- Makefile start/update pre-create cell-network idempotently
- reinstall/uninstall(full) explicitly delete and recreate the network
- Fix uninstall loop path: data/api/services/ (not data/services/)
Co-Authored-By: Claude Sonnet 4.6
---
Makefile | 4 +--
api/routes/peer_dashboard.py | 5 ++--
api/routes/wireguard.py | 50 ++++++++++++++++++++++++++++---
webui/src/pages/WireGuard.jsx | 55 +++++++++++++++++++++++++++++++++--
webui/src/services/api.js | 2 ++
5 files changed, 106 insertions(+), 10 deletions(-)
diff --git a/Makefile b/Makefile
index 777935d..74c0b81 100644
--- a/Makefile
+++ b/Makefile
@@ -180,7 +180,7 @@ uninstall:
case "$$ans" in \
y|Y) \
echo "Stopping containers and removing images..."; \
- for f in data/services/*/docker-compose.yml; do [ -f "$$f" ] && PUID=$$(id -u) PGID=$$(id -g) docker compose -f "$$f" down 2>/dev/null || true; done; \
+ for f in data/api/services/*/docker-compose.yml; do [ -f "$$f" ] && PUID=$$(id -u) PGID=$$(id -g) docker compose -f "$$f" down 2>/dev/null || true; done; \
PUID=$$(id -u) PGID=$$(id -g) $(DCF) --profile core down --rmi all 2>/dev/null || true; \
docker network rm cell-network 2>/dev/null || true; \
echo "Deleting config/ and data/..."; \
@@ -189,7 +189,7 @@ uninstall:
;; \
n|N|"") \
echo "Stopping and removing containers (keeping images and data)..."; \
- for f in data/services/*/docker-compose.yml; do [ -f "$$f" ] && PUID=$$(id -u) PGID=$$(id -g) docker compose -f "$$f" down 2>/dev/null || true; done; \
+ for f in data/api/services/*/docker-compose.yml; do [ -f "$$f" ] && PUID=$$(id -u) PGID=$$(id -g) docker compose -f "$$f" down 2>/dev/null || true; done; \
PUID=$$(id -u) PGID=$$(id -g) $(DCF) --profile core down 2>/dev/null || true; \
echo "Done. Images, config/ and data/ are untouched. Run 'make start' to bring it back up."; \
;; \
diff --git a/api/routes/peer_dashboard.py b/api/routes/peer_dashboard.py
index 0ed8416..1e95f84 100644
--- a/api/routes/peer_dashboard.py
+++ b/api/routes/peer_dashboard.py
@@ -65,10 +65,11 @@ def peer_services():
wg_port = 51820
server_endpoint = ''
try:
+ from routes.wireguard import _effective_endpoint
+ from app import config_manager
server_public_key = wireguard_manager.get_keys().get('public_key', '')
wg_port = config_manager.configs.get('_identity', {}).get('wireguard_port', 51820)
- srv = wireguard_manager.get_server_config()
- server_endpoint = srv.get('endpoint') or ''
+ server_endpoint = _effective_endpoint(wireguard_manager, config_manager)
except Exception:
pass
diff --git a/api/routes/wireguard.py b/api/routes/wireguard.py
index 820df4d..ddfccf4 100644
--- a/api/routes/wireguard.py
+++ b/api/routes/wireguard.py
@@ -4,6 +4,20 @@ from flask import Blueprint, request, jsonify
logger = logging.getLogger('picell')
bp = Blueprint('wireguard', __name__)
+
+def _effective_endpoint(wireguard_manager, config_manager) -> str:
+ """Return the WireGuard endpoint to embed in peer configs.
+
+ Uses wireguard_endpoint from identity config when set (admin override),
+ falling back to get_external_ip() detection.
+ """
+ srv = wireguard_manager.get_server_config()
+ override = (config_manager.get_identity().get('wireguard_endpoint') or '').strip()
+ if override:
+ port = srv.get('port', 51820)
+ return override if ':' in override else f'{override}:{port}'
+ return srv.get('endpoint') or ''
+
@bp.route('/api/wireguard/keys', methods=['GET'])
def get_wireguard_keys():
try:
@@ -171,8 +185,8 @@ def get_peer_config():
server_endpoint = data.get('server_endpoint', '')
if not server_endpoint:
- srv = wireguard_manager.get_server_config()
- server_endpoint = srv.get('endpoint') or ''
+ from app import config_manager
+ server_endpoint = _effective_endpoint(wireguard_manager, config_manager)
allowed_ips = data.get('allowed_ips') or None
if not allowed_ips and registered:
@@ -198,12 +212,40 @@ def get_peer_config():
@bp.route('/api/wireguard/server-config', methods=['GET'])
def get_server_config():
try:
- from app import wireguard_manager
- return jsonify(wireguard_manager.get_server_config())
+ from app import wireguard_manager, config_manager
+ cfg = wireguard_manager.get_server_config()
+ cfg['endpoint_override'] = (config_manager.get_identity().get('wireguard_endpoint') or '').strip()
+ cfg['effective_endpoint'] = _effective_endpoint(wireguard_manager, config_manager)
+ return jsonify(cfg)
except Exception as e:
logger.error(f"Error getting server config: {e}")
return jsonify({"error": str(e)}), 500
+@bp.route('/api/wireguard/endpoint', methods=['GET'])
+def get_wireguard_endpoint():
+ try:
+ from app import wireguard_manager, config_manager
+ return jsonify({
+ 'endpoint_override': (config_manager.get_identity().get('wireguard_endpoint') or '').strip(),
+ 'detected_endpoint': wireguard_manager.get_server_config().get('endpoint'),
+ 'effective_endpoint': _effective_endpoint(wireguard_manager, config_manager),
+ })
+ except Exception as e:
+ logger.error(f"Error getting wireguard endpoint: {e}")
+ return jsonify({"error": str(e)}), 500
+
+@bp.route('/api/wireguard/endpoint', methods=['PUT'])
+def set_wireguard_endpoint():
+ try:
+ from app import config_manager
+ data = request.get_json(silent=True) or {}
+ override = (data.get('endpoint_override') or '').strip()
+ config_manager.set_identity_field('wireguard_endpoint', override)
+ return jsonify({'endpoint_override': override, 'ok': True})
+ except Exception as e:
+ logger.error(f"Error setting wireguard endpoint: {e}")
+ return jsonify({"error": str(e)}), 500
+
@bp.route('/api/wireguard/refresh-ip', methods=['GET', 'POST'])
def refresh_external_ip():
try:
diff --git a/webui/src/pages/WireGuard.jsx b/webui/src/pages/WireGuard.jsx
index 3e6574f..6470a16 100644
--- a/webui/src/pages/WireGuard.jsx
+++ b/webui/src/pages/WireGuard.jsx
@@ -20,6 +20,10 @@ function WireGuard() {
const [qrCodeDataUrl, setQrCodeDataUrl] = useState('');
const [peerStatuses, setPeerStatuses] = useState({});
const [tunnelMode, setTunnelMode] = useState('full'); // 'split' or 'full'
+ const [endpointOverride, setEndpointOverride] = useState('');
+ const [endpointOverrideDraft, setEndpointOverrideDraft] = useState('');
+ const [isSavingEndpoint, setIsSavingEndpoint] = useState(false);
+ const [endpointSaved, setEndpointSaved] = useState(false);
useEffect(() => {
fetchWireGuardData();
@@ -51,6 +55,22 @@ function WireGuard() {
return () => clearInterval(interval);
}, []);
+ const saveEndpointOverride = async () => {
+ setIsSavingEndpoint(true);
+ try {
+ await wireguardAPI.setEndpointOverride(endpointOverrideDraft);
+ setEndpointOverride(endpointOverrideDraft);
+ setEndpointSaved(true);
+ setTimeout(() => setEndpointSaved(false), 3000);
+ const sc = await fetch('/api/wireguard/server-config', { credentials: 'include' }).then(r => r.json());
+ setServerConfig(prev => ({ ...prev, ...sc }));
+ } catch (e) {
+ console.error('Failed to save endpoint override:', e);
+ } finally {
+ setIsSavingEndpoint(false);
+ }
+ };
+
const refreshExternalIp = async () => {
setIsRefreshingIp(true);
try {
@@ -81,6 +101,9 @@ function WireGuard() {
setStatus(statusResponse.data);
if (serverConfigResponse) {
setServerConfig({ ...serverConfigResponse, port_open: 'checking' });
+ const override = serverConfigResponse.endpoint_override || '';
+ setEndpointOverride(override);
+ setEndpointOverrideDraft(override);
// Check port asynchronously so page loads fast
fetch('/api/wireguard/check-port', { credentials: 'include' })
.then(r => r.json())
@@ -446,10 +469,38 @@ PersistentKeepalive = ${peer.persistent_keepalive || 25}`;
- {serverConfig && !serverConfig.external_ip && (
+
+
Endpoint Override
+
+ Force a specific endpoint (IP or hostname) for peer configs. Leave blank to use auto-detected IP above.
+ Useful when peers connect via LAN or a different IP than what's detected.
+
+
+ setEndpointOverrideDraft(e.target.value)}
+ placeholder="e.g. 192.168.1.100 or myvpn.example.com"
+ className="flex-1 input text-sm font-mono"
+ />
+
+ {endpointSaved ? 'Saved' : isSavingEndpoint ? 'Saving…' : 'Save'}
+
+
+ {serverConfig?.effective_endpoint && (
+
+ Effective endpoint used in peer configs: {serverConfig.effective_endpoint}
+
+ )}
+
+ {serverConfig && !serverConfig.external_ip && !endpointOverride && (
- External IP could not be detected. Check internet connectivity, then click Refresh IP.
+ External IP could not be detected. Set an endpoint override above or check internet connectivity.
)}
{serverConfig && serverConfig.port_open === false && (
diff --git a/webui/src/services/api.js b/webui/src/services/api.js
index 3441672..4694e75 100644
--- a/webui/src/services/api.js
+++ b/webui/src/services/api.js
@@ -137,6 +137,8 @@ export const wireguardAPI = {
updatePeerIP: (data) => api.put('/api/wireguard/peers/ip', data),
getPeerConfig: (data) => api.post('/api/wireguard/peers/config', data),
getPeerStatuses: () => api.get('/api/wireguard/peers/statuses'),
+ getEndpoint: () => api.get('/api/wireguard/endpoint'),
+ setEndpointOverride: (endpoint_override) => api.put('/api/wireguard/endpoint', { endpoint_override }),
};
// Peer Registry API