fix: sync API key-store from wg0.conf to prevent WireGuard handshake failure
linuxserver/wireguard auto-generates its own PrivateKey on first container start, independently of the PIC API's key-store. When the two diverge, the API generates peer configs with the wrong server public key and the WireGuard handshake fails silently — the client can ping the VPN subnet (10.0.0.x) but gets no internet and cannot reach any Docker service (172.20.0.x). Adds _sync_keys_from_conf(): called at the top of apply_config(), reads the PrivateKey from wg0.conf, derives the matching public key, and overwrites the API key files (private.key / public.key) if they differ. This makes wg0.conf the authoritative source for the server identity, keeping get_peer_config() consistent with the live WireGuard interface. Adds 5 new tests in TestSyncKeysFromConf covering: - key-store update when conf key differs - no-op when keys already match - get_peer_config() uses the synced key - no raise when conf is missing - apply_config() passes the synced key through bootstrap Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -223,6 +223,45 @@ class WireGuardManager(BaseServiceManager):
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def _sync_keys_from_conf(self) -> None:
|
||||
"""Sync the API's key store from wg0.conf so both agree on the server identity.
|
||||
|
||||
linuxserver/wireguard auto-generates a PrivateKey on first container start.
|
||||
The API generates its own key independently. Any time apply_config() runs,
|
||||
read the PrivateKey from wg0.conf (the container's authoritative source) and
|
||||
update the API's key-store files to match — keeping get_keys() consistent.
|
||||
"""
|
||||
import base64 as _b64
|
||||
cf = self._config_file()
|
||||
if not os.path.exists(cf):
|
||||
return
|
||||
try:
|
||||
with open(cf) as f:
|
||||
raw = f.read()
|
||||
for line in raw.splitlines():
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('PrivateKey'):
|
||||
conf_priv = stripped.split('=', 1)[1].strip()
|
||||
api_keys = self.get_keys()
|
||||
if conf_priv == api_keys.get('private_key'):
|
||||
return # already in sync
|
||||
# Derive public key from private key and update both files
|
||||
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
|
||||
priv_bytes = _b64.b64decode(conf_priv)
|
||||
priv_obj = X25519PrivateKey.from_private_bytes(priv_bytes)
|
||||
pub_bytes = priv_obj.public_key().public_bytes_raw()
|
||||
pub_b64 = _b64.b64encode(pub_bytes).decode()
|
||||
priv_file = os.path.join(self.keys_dir, 'private.key')
|
||||
pub_file = os.path.join(self.keys_dir, 'public.key')
|
||||
with open(priv_file, 'wb') as f:
|
||||
f.write(priv_bytes)
|
||||
with open(pub_file, 'wb') as f:
|
||||
f.write(pub_bytes)
|
||||
logger.info(f'wg: key-store synced from wg0.conf (new pub={pub_b64[:16]}...)')
|
||||
return
|
||||
except Exception as e:
|
||||
logger.warning(f'_sync_keys_from_conf failed (non-fatal): {e}')
|
||||
|
||||
def apply_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Update wg0.conf interface fields and restart cell-wireguard."""
|
||||
restarted = []
|
||||
@@ -232,6 +271,11 @@ class WireGuardManager(BaseServiceManager):
|
||||
warnings.append('wg0.conf not found — skipping')
|
||||
return {'restarted': restarted, 'warnings': warnings}
|
||||
try:
|
||||
# Sync the API key-store from wg0.conf before doing anything else.
|
||||
# linuxserver/wireguard auto-generates its own key; this keeps both in sync
|
||||
# so get_peer_config() always embeds the correct server public key.
|
||||
self._sync_keys_from_conf()
|
||||
|
||||
with open(cf) as f:
|
||||
raw = f.read()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user