feat: WireGuard endpoint override + fix Docker network label issue
Unit Tests / test (push) Successful in 11m14s
Unit Tests / test (push) Successful in 11m14s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -180,7 +180,7 @@ uninstall:
|
|||||||
case "$$ans" in \
|
case "$$ans" in \
|
||||||
y|Y) \
|
y|Y) \
|
||||||
echo "Stopping containers and removing images..."; \
|
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; \
|
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; \
|
docker network rm cell-network 2>/dev/null || true; \
|
||||||
echo "Deleting config/ and data/..."; \
|
echo "Deleting config/ and data/..."; \
|
||||||
@@ -189,7 +189,7 @@ uninstall:
|
|||||||
;; \
|
;; \
|
||||||
n|N|"") \
|
n|N|"") \
|
||||||
echo "Stopping and removing containers (keeping images and data)..."; \
|
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; \
|
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."; \
|
echo "Done. Images, config/ and data/ are untouched. Run 'make start' to bring it back up."; \
|
||||||
;; \
|
;; \
|
||||||
|
|||||||
@@ -65,10 +65,11 @@ def peer_services():
|
|||||||
wg_port = 51820
|
wg_port = 51820
|
||||||
server_endpoint = ''
|
server_endpoint = ''
|
||||||
try:
|
try:
|
||||||
|
from routes.wireguard import _effective_endpoint
|
||||||
|
from app import config_manager
|
||||||
server_public_key = wireguard_manager.get_keys().get('public_key', '')
|
server_public_key = wireguard_manager.get_keys().get('public_key', '')
|
||||||
wg_port = config_manager.configs.get('_identity', {}).get('wireguard_port', 51820)
|
wg_port = config_manager.configs.get('_identity', {}).get('wireguard_port', 51820)
|
||||||
srv = wireguard_manager.get_server_config()
|
server_endpoint = _effective_endpoint(wireguard_manager, config_manager)
|
||||||
server_endpoint = srv.get('endpoint') or '<SERVER_IP>'
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
+46
-4
@@ -4,6 +4,20 @@ from flask import Blueprint, request, jsonify
|
|||||||
logger = logging.getLogger('picell')
|
logger = logging.getLogger('picell')
|
||||||
bp = Blueprint('wireguard', __name__)
|
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 '<SERVER_IP>'
|
||||||
|
|
||||||
@bp.route('/api/wireguard/keys', methods=['GET'])
|
@bp.route('/api/wireguard/keys', methods=['GET'])
|
||||||
def get_wireguard_keys():
|
def get_wireguard_keys():
|
||||||
try:
|
try:
|
||||||
@@ -171,8 +185,8 @@ def get_peer_config():
|
|||||||
|
|
||||||
server_endpoint = data.get('server_endpoint', '')
|
server_endpoint = data.get('server_endpoint', '')
|
||||||
if not server_endpoint:
|
if not server_endpoint:
|
||||||
srv = wireguard_manager.get_server_config()
|
from app import config_manager
|
||||||
server_endpoint = srv.get('endpoint') or '<SERVER_IP>'
|
server_endpoint = _effective_endpoint(wireguard_manager, config_manager)
|
||||||
|
|
||||||
allowed_ips = data.get('allowed_ips') or None
|
allowed_ips = data.get('allowed_ips') or None
|
||||||
if not allowed_ips and registered:
|
if not allowed_ips and registered:
|
||||||
@@ -198,12 +212,40 @@ def get_peer_config():
|
|||||||
@bp.route('/api/wireguard/server-config', methods=['GET'])
|
@bp.route('/api/wireguard/server-config', methods=['GET'])
|
||||||
def get_server_config():
|
def get_server_config():
|
||||||
try:
|
try:
|
||||||
from app import wireguard_manager
|
from app import wireguard_manager, config_manager
|
||||||
return jsonify(wireguard_manager.get_server_config())
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Error getting server config: {e}")
|
logger.error(f"Error getting server config: {e}")
|
||||||
return jsonify({"error": str(e)}), 500
|
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'])
|
@bp.route('/api/wireguard/refresh-ip', methods=['GET', 'POST'])
|
||||||
def refresh_external_ip():
|
def refresh_external_ip():
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ function WireGuard() {
|
|||||||
const [qrCodeDataUrl, setQrCodeDataUrl] = useState('');
|
const [qrCodeDataUrl, setQrCodeDataUrl] = useState('');
|
||||||
const [peerStatuses, setPeerStatuses] = useState({});
|
const [peerStatuses, setPeerStatuses] = useState({});
|
||||||
const [tunnelMode, setTunnelMode] = useState('full'); // 'split' or 'full'
|
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(() => {
|
useEffect(() => {
|
||||||
fetchWireGuardData();
|
fetchWireGuardData();
|
||||||
@@ -51,6 +55,22 @@ function WireGuard() {
|
|||||||
return () => clearInterval(interval);
|
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 () => {
|
const refreshExternalIp = async () => {
|
||||||
setIsRefreshingIp(true);
|
setIsRefreshingIp(true);
|
||||||
try {
|
try {
|
||||||
@@ -81,6 +101,9 @@ function WireGuard() {
|
|||||||
setStatus(statusResponse.data);
|
setStatus(statusResponse.data);
|
||||||
if (serverConfigResponse) {
|
if (serverConfigResponse) {
|
||||||
setServerConfig({ ...serverConfigResponse, port_open: 'checking' });
|
setServerConfig({ ...serverConfigResponse, port_open: 'checking' });
|
||||||
|
const override = serverConfigResponse.endpoint_override || '';
|
||||||
|
setEndpointOverride(override);
|
||||||
|
setEndpointOverrideDraft(override);
|
||||||
// Check port asynchronously so page loads fast
|
// Check port asynchronously so page loads fast
|
||||||
fetch('/api/wireguard/check-port', { credentials: 'include' })
|
fetch('/api/wireguard/check-port', { credentials: 'include' })
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
@@ -446,10 +469,38 @@ PersistentKeepalive = ${peer.persistent_keepalive || 25}`;
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{serverConfig && !serverConfig.external_ip && (
|
<div className="mt-4 border-t pt-4">
|
||||||
|
<p className="text-sm font-medium text-gray-700 mb-1">Endpoint Override</p>
|
||||||
|
<p className="text-xs text-gray-500 mb-2">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={endpointOverrideDraft}
|
||||||
|
onChange={e => setEndpointOverrideDraft(e.target.value)}
|
||||||
|
placeholder="e.g. 192.168.1.100 or myvpn.example.com"
|
||||||
|
className="flex-1 input text-sm font-mono"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={saveEndpointOverride}
|
||||||
|
disabled={isSavingEndpoint || endpointOverrideDraft === endpointOverride}
|
||||||
|
className="btn btn-primary text-sm"
|
||||||
|
>
|
||||||
|
{endpointSaved ? 'Saved' : isSavingEndpoint ? 'Saving…' : 'Save'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{serverConfig?.effective_endpoint && (
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Effective endpoint used in peer configs: <span className="font-mono font-medium">{serverConfig.effective_endpoint}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{serverConfig && !serverConfig.external_ip && !endpointOverride && (
|
||||||
<div className="mt-3 flex items-center text-yellow-700 bg-yellow-50 rounded p-2 text-sm">
|
<div className="mt-3 flex items-center text-yellow-700 bg-yellow-50 rounded p-2 text-sm">
|
||||||
<AlertCircle className="h-4 w-4 mr-2 flex-shrink-0" />
|
<AlertCircle className="h-4 w-4 mr-2 flex-shrink-0" />
|
||||||
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.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{serverConfig && serverConfig.port_open === false && (
|
{serverConfig && serverConfig.port_open === false && (
|
||||||
|
|||||||
@@ -137,6 +137,8 @@ export const wireguardAPI = {
|
|||||||
updatePeerIP: (data) => api.put('/api/wireguard/peers/ip', data),
|
updatePeerIP: (data) => api.put('/api/wireguard/peers/ip', data),
|
||||||
getPeerConfig: (data) => api.post('/api/wireguard/peers/config', data),
|
getPeerConfig: (data) => api.post('/api/wireguard/peers/config', data),
|
||||||
getPeerStatuses: () => api.get('/api/wireguard/peers/statuses'),
|
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
|
// Peer Registry API
|
||||||
|
|||||||
Reference in New Issue
Block a user