1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 02:59:00 +03:00
This commit is contained in:
Anton Afanasyeu
2026-06-13 16:49:40 +02:00
parent 86055bf509
commit 2fd6d421a3
11 changed files with 198 additions and 18 deletions

View File

@@ -295,6 +295,7 @@ mysql -u root -p androidcast_crashes < sql/migrations/007_remote_access.sql
```cron
0 * * * * cd /var/www/localhost/htdocs/apps/app/androidcast_project/android_cast/examples/crash_reporter/backend && php81 scripts/purge_remote_access.php --hours=24
# Prunes orphan wg0 peers each run; manual: php81 scripts/sync_wg_peers.php --dry-run
```
Smoke / verify:

View File

@@ -4,19 +4,27 @@ declare(strict_types=1);
/**
* Purge stale remote-access sessions (cron: hourly).
* Usage: php scripts/purge_remote_access.php [--hours=24]
* Usage: php scripts/purge_remote_access.php [--hours=24] [--no-prune-wg]
*/
require_once __DIR__ . '/../src/bootstrap.php';
require_once __DIR__ . '/../src/RemoteAccessRepository.php';
require_once __DIR__ . '/../src/WireGuardPeerProvisioner.php';
$hours = 24;
$pruneWg = true;
foreach ($argv as $arg) {
if (str_starts_with($arg, '--hours=')) {
$hours = max(1, (int) substr($arg, 8));
} elseif ($arg === '--no-prune-wg') {
$pruneWg = false;
}
}
RemoteAccessRepository::ensureSchema();
$n = RemoteAccessRepository::purgeStale($hours);
fwrite(STDOUT, "purged_sessions={$n}\n");
if ($pruneWg) {
$pruned = RemoteAccessRepository::pruneOrphanWireGuardPeers(false);
fwrite(STDOUT, 'pruned_wg_peers=' . count($pruned) . "\n");
}

View File

@@ -87,7 +87,7 @@ ra_wg_quick_from_connect() {
fi
local priv addr peer_pub endpoint allowed
priv=$(echo "$connect_json" | jq -r '.heartbeat.credentials.interface_private_key // empty')
addr=$(echo "$connect_json" | jq -r '.heartbeat.credentials.interface_address // "10.66.66.2/32"')
addr=$(echo "$connect_json" | jq -r '.heartbeat.credentials.interface_address // "172.200.2.10/32"')
peer_pub=$(echo "$connect_json" | jq -r '.heartbeat.credentials.peer_public_key // empty')
endpoint=$(echo "$connect_json" | jq -r '.heartbeat.endpoint // .heartbeat.credentials.peer_endpoint // empty')
allowed=$(echo "$connect_json" | jq -r '.heartbeat.credentials.peer_allowed_ips // "0.0.0.0/0"')

View File

@@ -22,8 +22,8 @@ Usage: $0 [--apply] [--device-id ID] [--skip-http]
Env:
CRASHES_BASE, ADMIN_USER, ADMIN_PASS — operator API
RA_DEVICE_ID, WG_HOST (default ra.apps.f0xx.org), WG_PORT (51820)
BE_WG_IP (10.66.66.1) — ping target on tunnel
RA_DEVICE_ID, WG_HOST (default ra.apps.f0xx.org), WG_PORT (45340)
BE_WG_IP (172.200.2.1) — ping target on tunnel
RA_UDP_APPLY=1 — same as --apply (wg-quick up)
Steps: DNS → UDP port → optional HTTP E2E → connect JSON → wg conf → optional tunnel → ping
@@ -116,7 +116,7 @@ if [[ "$APPLY" -eq 1 ]]; then
if ping -c 3 -W 2 "$BE_WG_IP"; then
ok "ping ${BE_WG_IP}"
else
warn "ping failed — routing or AllowedIPs (expected 10.66.66.1/32)"
warn "ping failed — routing or AllowedIPs (expected 172.200.2.1/32)"
fi
note "9. Teardown"
sudo wg-quick down "$CONF"

View File

@@ -0,0 +1,46 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
/**
* Reconcile wg0 peers with pending/active remote-access sessions.
*
* Usage:
* php scripts/sync_wg_peers.php [--dry-run]
* php scripts/sync_wg_peers.php --apply
*/
require_once __DIR__ . '/../src/bootstrap.php';
require_once __DIR__ . '/../src/RemoteAccessRepository.php';
$dryRun = true;
foreach ($argv as $arg) {
if ($arg === '--apply') {
$dryRun = false;
} elseif ($arg === '--dry-run' || $arg === '-n') {
$dryRun = true;
} elseif ($arg === '-h' || $arg === '--help') {
fwrite(STDOUT, "Usage: php scripts/sync_wg_peers.php [--dry-run|--apply]\n");
exit(0);
}
}
RemoteAccessRepository::ensureSchema();
$keep = RemoteAccessRepository::activeWireGuardSessionPublicKeys();
$pruned = RemoteAccessRepository::pruneOrphanWireGuardPeers($dryRun);
fwrite(STDOUT, 'mode=' . ($dryRun ? 'dry-run' : 'apply') . "\n");
fwrite(STDOUT, 'active_session_peers=' . count($keep) . "\n");
fwrite(STDOUT, 'pruned_peers=' . count($pruned) . "\n");
foreach ($pruned as $row) {
$pub = $row['public_key'] ?? '';
$short = strlen($pub) > 12 ? substr($pub, 0, 8) . '…' : $pub;
fwrite(
STDOUT,
sprintf(
" %s reason=%s allowed_ips=%s\n",
$short,
(string) ($row['reason'] ?? ''),
(string) ($row['allowed_ips'] ?? '')
)
);
}

View File

@@ -97,6 +97,19 @@ foreach (["remote_access_devices","remote_access_sessions","remote_access_events
echo "OK MariaDB/SQLite remote_access tables present\n";
' || fail=1
# Orphan wg peers (dry-run; run sync_wg_peers.php --apply on BE to fix)
if [[ -f "$ROOT/scripts/sync_wg_peers.php" ]]; then
WG_SYNC="$($DB_RUN "$ROOT/scripts/sync_wg_peers.php" --dry-run 2>/dev/null || true)"
if [[ -n "$WG_SYNC" ]]; then
pruned=$(echo "$WG_SYNC" | awk -F= '/^pruned_peers=/{print $2}')
if [[ -n "$pruned" && "$pruned" != "0" ]]; then
note "wg0 has ${pruned} orphan peer(s) — run: php81 scripts/sync_wg_peers.php --apply"
else
ok "wg0 peers in sync with active sessions"
fi
fi
fi
# API smoke (local php -S or deployed BASE)
BASE="${BASE:-${CRASHES_BASE:-http://127.0.0.1:8080/app/androidcast_project/crashes}}"
if [[ -x "$ROOT/scripts/test_remote_access_api.sh" ]]; then

View File

@@ -756,6 +756,39 @@ final class RemoteAccessRepository {
return true;
}
/** @return list<string> */
public static function activeWireGuardSessionPublicKeys(): array {
self::ensureSchema();
$st = Database::pdo()->query(
"SELECT DISTINCT wg_client_public_key FROM remote_access_sessions
WHERE status IN ('pending','active') AND tunnel = 'wireguard'
AND wg_client_public_key IS NOT NULL AND wg_client_public_key != ''"
);
if ($st === false) {
return [];
}
$keys = [];
foreach ($st->fetchAll(PDO::FETCH_COLUMN) as $pub) {
$pub = trim((string) $pub);
if ($pub !== '') {
$keys[] = $pub;
}
}
return $keys;
}
/**
* Drop wg0 peers that are not referenced by pending/active sessions.
*
* @return list<array{public_key: string, allowed_ips: string, reason: string}>
*/
public static function pruneOrphanWireGuardPeers(bool $dryRun = false): array {
return WireGuardPeerProvisioner::pruneOrphanPeers(
self::activeWireGuardSessionPublicKeys(),
$dryRun
);
}
public static function purgeStale(int $maxAgeHours = 24): int {
self::ensureSchema();
$cutoff = time() - max(1, $maxAgeHours) * 3600;

View File

@@ -59,6 +59,77 @@ final class WireGuardPeerProvisioner {
);
}
/** @return list<array{public_key: string, allowed_ips: string}> */
public static function listPeerDumpRows(): array {
if (!self::isEnabled()) {
return [];
}
$iface = self::interfaceName();
$prefix = rtrim((string) cfg('remote_access.wg_set_prefix', ''), " \t");
$cmd = ($prefix !== '' ? $prefix . ' ' : '') . 'wg show ' . escapeshellarg($iface) . ' dump 2>/dev/null';
$raw = @shell_exec($cmd);
if (!is_string($raw) || trim($raw) === '') {
return [];
}
$rows = [];
foreach (preg_split('/\r\n|\n|\r/', trim($raw)) as $line) {
if ($line === '' || str_starts_with($line, 'interface:')) {
continue;
}
$cols = explode("\t", $line);
if (count($cols) < 4) {
continue;
}
$pub = trim($cols[0]);
if ($pub === '') {
continue;
}
$rows[] = [
'public_key' => $pub,
'allowed_ips' => trim((string) ($cols[3] ?? '')),
];
}
return $rows;
}
/**
* Remove live wg peers not tied to pending/active sessions (or with empty AllowedIPs).
*
* @param list<string> $keepPublicKeys
* @return list<array{public_key: string, allowed_ips: string, reason: string}>
*/
public static function pruneOrphanPeers(array $keepPublicKeys, bool $dryRun = false): array {
if (!self::isEnabled()) {
return [];
}
$keep = [];
foreach ($keepPublicKeys as $key) {
$pub = trim((string) $key);
if ($pub !== '') {
$keep[$pub] = true;
}
}
$pruned = [];
foreach (self::listPeerDumpRows() as $row) {
$pub = $row['public_key'];
$allowed = trim($row['allowed_ips']);
$broken = ($allowed === '' || $allowed === '(none)');
$orphan = !isset($keep[$pub]);
if (!$orphan && !$broken) {
continue;
}
if (!$dryRun) {
self::removeClientPeer($pub);
}
$pruned[] = [
'public_key' => $pub,
'allowed_ips' => $allowed,
'reason' => $broken ? 'broken_allowed_ips' : 'orphan',
];
}
return $pruned;
}
private static function runWg(string $args): int {
$prefix = rtrim((string) cfg('remote_access.wg_set_prefix', ''), " \t");
$cmd = ($prefix !== '' ? $prefix . ' ' : '') . 'wg ' . $args;