mirror of
git://f0xx.org/android_cast
synced 2026-07-29 07:39:15 +03:00
sync
This commit is contained in:
@@ -49,7 +49,7 @@ WireGuard on Android **must** use `VpnService` (see `DeveloperSettingsActivity`
|
|||||||
| One-time system dialog | Yes — “Allow VPN?” | No VPN permission |
|
| One-time system dialog | Yes — “Allow VPN?” | No VPN permission |
|
||||||
| Status-bar key icon | Yes while tunnel up | No |
|
| Status-bar key icon | Yes while tunnel up | No |
|
||||||
| Full-device routing | TUN interface (split routes in config, still a VPN) | No L3 tunnel |
|
| Full-device routing | TUN interface (split routes in config, still a VPN) | No L3 tunnel |
|
||||||
| Outbound from phone | UDP to BE `:51820` | HTTPS poll + outbound SSH to bastion |
|
| Outbound from phone | UDP to BE `:45340` | HTTPS poll + outbound SSH to bastion |
|
||||||
|
|
||||||
There is no supported “hidden VPN” on non-root Android: any TUN creator is classified as a VPN app. **RSSH** (foreground service + existing HTTPS poll + outbound SSH reverse tunnel) is the **alpha deliverable** agreed by project owners — device initiates, operator reaches a forwarded port on the bastion, without VPN consent or status-bar key. See [Rev. 3 Q&A](20260602_REVERSE_SSH_proposals_summary.md#rev-3--wg-vs-ssh-qa--bastion-deep-dive) and [ROADMAP.md § Remote access](ROADMAP.md#remote-access-alpha--rssh).
|
There is no supported “hidden VPN” on non-root Android: any TUN creator is classified as a VPN app. **RSSH** (foreground service + existing HTTPS poll + outbound SSH reverse tunnel) is the **alpha deliverable** agreed by project owners — device initiates, operator reaches a forwarded port on the bastion, without VPN consent or status-bar key. See [Rev. 3 Q&A](20260602_REVERSE_SSH_proposals_summary.md#rev-3--wg-vs-ssh-qa--bastion-deep-dive) and [ROADMAP.md § Remote access](ROADMAP.md#remote-access-alpha--rssh).
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ Migration **007** (already applied on prod per deploy notes). Tables auto-create
|
|||||||
|
|
||||||
```php
|
```php
|
||||||
'remote_access' => [
|
'remote_access' => [
|
||||||
'wg_endpoint' => 'ra.apps.f0xx.org:51820', // public UDP hostname:port (FE DNAT → BE)
|
'wg_endpoint' => 'ra.apps.f0xx.org:45340', // public UDP hostname:port (FE DNAT → BE)
|
||||||
'wg_server_public_key' => '…', // wg show wg0 public-key on BE — REQUIRED
|
'wg_server_public_key' => '…', // wg show wg0 public-key on BE — REQUIRED
|
||||||
'wg_interface' => 'wg0',
|
'wg_interface' => 'wg0',
|
||||||
'provision_peers' => true, // wg set on connect/close
|
'provision_peers' => true, // wg set on connect/close
|
||||||
@@ -121,7 +121,7 @@ Migration **007** (already applied on prod per deploy notes). Tables auto-create
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
apk add wireguard-tools wireguard-tools-wg
|
apk add wireguard-tools wireguard-tools-wg
|
||||||
# wg0 configured separately (Address 10.66.66.1/24, ListenPort 51820, etc.)
|
# wg0 configured separately (Address 172.200.2.1/16, ListenPort 45340, etc.)
|
||||||
|
|
||||||
# PHP-FPM user must run wg when provision_peers=true (same as CLI test):
|
# PHP-FPM user must run wg when provision_peers=true (same as CLI test):
|
||||||
sudo -u www-data wg show wg0
|
sudo -u www-data wg show wg0
|
||||||
@@ -131,6 +131,7 @@ sudo -u www-data wg show wg0
|
|||||||
|
|
||||||
```cron
|
```cron
|
||||||
0 * * * * cd /var/www/.../backend && php81 scripts/purge_remote_access.php --hours=24
|
0 * * * * cd /var/www/.../backend && php81 scripts/purge_remote_access.php --hours=24
|
||||||
|
# Orphan wg peers are pruned each run; skip with --no-prune-wg. Manual: php81 scripts/sync_wg_peers.php --dry-run
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `php81` when Alpine default `php` is 8.5+ without the session module; FPM is `php-fpm81`.
|
Use `php81` when Alpine default `php` is 8.5+ without the session module; FPM is `php-fpm81`.
|
||||||
@@ -156,7 +157,7 @@ Hard-refresh after deploying `public/assets/js/remote_access.js`.
|
|||||||
HTTP control plane stays on FE→BE `:80`. **WireGuard traffic is UDP** — configure at the **FE firewall/DNAT**, not in nginx:
|
HTTP control plane stays on FE→BE `:80`. **WireGuard traffic is UDP** — configure at the **FE firewall/DNAT**, not in nginx:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Internet UDP :51820 → FE DNAT → BE wg0 :51820
|
Internet UDP :45340 → FE DNAT → BE wg0 :45340
|
||||||
```
|
```
|
||||||
|
|
||||||
Hostname `ra.apps.f0xx.org` (or similar) should resolve to the FE public IP. Devices use `wg_endpoint` from config/connect payload.
|
Hostname `ra.apps.f0xx.org` (or similar) should resolve to the FE public IP. Devices use `wg_endpoint` from config/connect payload.
|
||||||
|
|||||||
@@ -55,8 +55,9 @@ All under `examples/crash_reporter/backend/scripts/`:
|
|||||||
| `ra_device_sim.sh` | Device-only: disable / enable / poll / optional WG |
|
| `ra_device_sim.sh` | Device-only: disable / enable / poll / optional WG |
|
||||||
| `ra_e2e_cli.sh` | Full flow: disable → wait → whitelist → open → connect → close → disable |
|
| `ra_e2e_cli.sh` | Full flow: disable → wait → whitelist → open → connect → close → disable |
|
||||||
| `test_remote_access_api.sh` | Minimal API smoke (grep, no jq) |
|
| `test_remote_access_api.sh` | Minimal API smoke (grep, no jq) |
|
||||||
| `verify_remote_access_prod.sh` | BE preflight: config, wg tools, DB tables, optional smoke |
|
| `verify_remote_access_prod.sh` | BE preflight: config, wg tools, DB tables, wg peer sync, optional smoke |
|
||||||
| `ra_udp_debug.sh` | DNS + UDP port + HTTP smoke + connect + `wg-quick` + ping BE (`10.66.66.1`) |
|
| `sync_wg_peers.php` | Reconcile wg0 peers with pending/active sessions (`--dry-run` / `--apply`) |
|
||||||
|
| `ra_udp_debug.sh` | DNS + UDP port + HTTP smoke + connect + `wg-quick` + ping BE (`172.200.2.1`) |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd examples/crash_reporter/backend
|
cd examples/crash_reporter/backend
|
||||||
@@ -91,7 +92,7 @@ export RA_DEVICE_ID=my-lab-phone
|
|||||||
|
|
||||||
## Production / staging BE
|
## Production / staging BE
|
||||||
|
|
||||||
**Verified 2026-06-04** on `https://apps.f0xx.org/app/androidcast_project/crashes`: `ra_e2e_cli.sh` passed (connect returned real `peer_public_key`, endpoint `ra.apps.f0xx.org:51820`). Poll step may take up to ~45s per `min_poll_interval_s`. UDP handshake still needs a device or `ra_device_sim.sh --apply-wg` from a reachable host.
|
**Verified 2026-06-04** on `https://apps.f0xx.org/app/androidcast_project/crashes`: `ra_e2e_cli.sh` passed (connect returned real `peer_public_key`, endpoint `ra.apps.f0xx.org:45340`). Poll step may take up to ~45s per `min_poll_interval_s`. UDP handshake still needs a device or `ra_device_sim.sh --apply-wg` from a reachable host.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh alpine-be # or your BE host
|
ssh alpine-be # or your BE host
|
||||||
@@ -130,10 +131,10 @@ export CRASHES_BASE=https://apps.f0xx.org/app/androidcast_project/crashes
|
|||||||
export ADMIN_USER=admin ADMIN_PASS='…'
|
export ADMIN_USER=admin ADMIN_PASS='…'
|
||||||
|
|
||||||
./scripts/ra_udp_debug.sh # DNS, UDP nc, HTTP smoke, connect, wg conf
|
./scripts/ra_udp_debug.sh # DNS, UDP nc, HTTP smoke, connect, wg conf
|
||||||
./scripts/ra_udp_debug.sh --apply # also wg-quick up + ping 10.66.66.1 + teardown
|
./scripts/ra_udp_debug.sh --apply # also wg-quick up + ping 172.200.2.1 + teardown
|
||||||
```
|
```
|
||||||
|
|
||||||
Steps: CNAME check (`ra.apps.f0xx.org` → same A as `apps.f0xx.org`) → UDP `51820` → `test_remote_access_api.sh` → whitelist + open + poll connect → write `/tmp/ra-udp-<id>.conf`. With `--apply`, brings tunnel up and pings the BE WG address.
|
Steps: CNAME check (`ra.apps.f0xx.org` → same A as `apps.f0xx.org`) → UDP `45340` → `test_remote_access_api.sh` → whitelist + open + poll connect → write `/tmp/ra-udp-<id>.conf`. With `--apply`, brings tunnel up and pings the BE WG address.
|
||||||
|
|
||||||
Skip HTTP re-check when control plane was just verified: `./scripts/ra_udp_debug.sh --skip-http --apply`.
|
Skip HTTP re-check when control plane was just verified: `./scripts/ra_udp_debug.sh --skip-http --apply`.
|
||||||
|
|
||||||
@@ -157,7 +158,7 @@ RESP="$(curl -sf …)" # connect response
|
|||||||
source scripts/ra_lib.sh
|
source scripts/ra_lib.sh
|
||||||
ra_wg_quick_from_connect "$RESP" | sudo tee /tmp/ra-test.conf
|
ra_wg_quick_from_connect "$RESP" | sudo tee /tmp/ra-test.conf
|
||||||
sudo wg-quick up /tmp/ra-test.conf
|
sudo wg-quick up /tmp/ra-test.conf
|
||||||
ping -c1 10.66.66.1 # BE wg0 address if routed
|
ping -c1 172.200.2.1 # BE wg0 address if routed
|
||||||
sudo wg-quick down /tmp/ra-test.conf
|
sudo wg-quick down /tmp/ra-test.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -9,20 +9,26 @@ See also [REMOTE_ACCESS_VALIDATION.md](../REMOTE_ACCESS_VALIDATION.md), [REMOTE_
|
|||||||
- **VPN traffic counters** in admin expanded device row (`WireGuardTrafficStats.php`)
|
- **VPN traffic counters** in admin expanded device row (`WireGuardTrafficStats.php`)
|
||||||
- sudoers: `wg show wg0 dump` for nginx FPM user
|
- sudoers: `wg show wg0 dump` for nginx FPM user
|
||||||
|
|
||||||
|
## Fixed in repo (awaiting BE sync)
|
||||||
|
|
||||||
|
- **Operational docs** — `REMOTE_ACCESS_IMPL.md` / `REMOTE_ACCESS_VALIDATION.md` now use prod **45340** and **172.200.2.0/16** (proposal doc still uses example 51820)
|
||||||
|
- **Orphan wg peer cleanup** — `WireGuardPeerProvisioner::pruneOrphanPeers()`, hourly via `purge_remote_access.php`; manual `scripts/sync_wg_peers.php --dry-run|--apply`
|
||||||
|
- **CLI defaults** — `ra_udp_debug.sh` / `ra_lib.sh` help and fallbacks match prod subnet
|
||||||
|
|
||||||
## Verified OK
|
## Verified OK
|
||||||
|
|
||||||
- `wg0` on BE, endpoint `ra.apps.f0xx.org:45340`
|
- `wg0` on BE, endpoint `ra.apps.f0xx.org:45340`
|
||||||
- UDP 45340 open on public IP
|
- UDP 45340 open on public IP
|
||||||
- Heartbeat disable/enable API
|
- Heartbeat disable/enable API
|
||||||
- Mobile boot restores RA mode (`RemoteAccessCoordinator.onBootIfNeeded`)
|
- Mobile boot restores RA mode (`RemoteAccessCoordinator.onBootIfNeeded`)
|
||||||
|
- App wait-state + diagnostics (idle vs fail-reason); 30s heartbeat timeout
|
||||||
|
|
||||||
## Open (PO / device)
|
## Open (PO / device)
|
||||||
|
|
||||||
1. Full `ra_e2e_cli.sh` — needs admin login on laptop
|
1. Full `ra_e2e_cli.sh` — needs admin login on laptop
|
||||||
2. UDP handshake + ping — needs phone or `ra_udp_debug.sh --apply`
|
2. UDP handshake + ping — needs phone or `ra_udp_debug.sh --apply`
|
||||||
3. SSH passthrough — RSSH track, not WG v1
|
3. SSH passthrough — RSSH track, not WG v1
|
||||||
4. Doc port references still say 51820 in places; prod uses **45340**
|
4. Persistent traffic history — live stats only today
|
||||||
5. Stale wg peer with `allowed ips: (none)` — manual cleanup
|
5. Whitelist + device E2E — human validation in progress
|
||||||
6. Persistent traffic history — live stats only today
|
|
||||||
|
|
||||||
Journal: `/shared/journal/cluster0.journal.tsv` (tag `vpn-demo`).
|
Journal: `/shared/journal/cluster0.journal.tsv` (tag `vpn-demo`).
|
||||||
|
|||||||
@@ -295,6 +295,7 @@ mysql -u root -p androidcast_crashes < sql/migrations/007_remote_access.sql
|
|||||||
|
|
||||||
```cron
|
```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
|
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:
|
Smoke / verify:
|
||||||
|
|||||||
@@ -4,19 +4,27 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Purge stale remote-access sessions (cron: hourly).
|
* 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/bootstrap.php';
|
||||||
require_once __DIR__ . '/../src/RemoteAccessRepository.php';
|
require_once __DIR__ . '/../src/RemoteAccessRepository.php';
|
||||||
require_once __DIR__ . '/../src/WireGuardPeerProvisioner.php';
|
require_once __DIR__ . '/../src/WireGuardPeerProvisioner.php';
|
||||||
|
|
||||||
$hours = 24;
|
$hours = 24;
|
||||||
|
$pruneWg = true;
|
||||||
foreach ($argv as $arg) {
|
foreach ($argv as $arg) {
|
||||||
if (str_starts_with($arg, '--hours=')) {
|
if (str_starts_with($arg, '--hours=')) {
|
||||||
$hours = max(1, (int) substr($arg, 8));
|
$hours = max(1, (int) substr($arg, 8));
|
||||||
|
} elseif ($arg === '--no-prune-wg') {
|
||||||
|
$pruneWg = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoteAccessRepository::ensureSchema();
|
RemoteAccessRepository::ensureSchema();
|
||||||
$n = RemoteAccessRepository::purgeStale($hours);
|
$n = RemoteAccessRepository::purgeStale($hours);
|
||||||
fwrite(STDOUT, "purged_sessions={$n}\n");
|
fwrite(STDOUT, "purged_sessions={$n}\n");
|
||||||
|
|
||||||
|
if ($pruneWg) {
|
||||||
|
$pruned = RemoteAccessRepository::pruneOrphanWireGuardPeers(false);
|
||||||
|
fwrite(STDOUT, 'pruned_wg_peers=' . count($pruned) . "\n");
|
||||||
|
}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ ra_wg_quick_from_connect() {
|
|||||||
fi
|
fi
|
||||||
local priv addr peer_pub endpoint allowed
|
local priv addr peer_pub endpoint allowed
|
||||||
priv=$(echo "$connect_json" | jq -r '.heartbeat.credentials.interface_private_key // empty')
|
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')
|
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')
|
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"')
|
allowed=$(echo "$connect_json" | jq -r '.heartbeat.credentials.peer_allowed_ips // "0.0.0.0/0"')
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ Usage: $0 [--apply] [--device-id ID] [--skip-http]
|
|||||||
|
|
||||||
Env:
|
Env:
|
||||||
CRASHES_BASE, ADMIN_USER, ADMIN_PASS — operator API
|
CRASHES_BASE, ADMIN_USER, ADMIN_PASS — operator API
|
||||||
RA_DEVICE_ID, WG_HOST (default ra.apps.f0xx.org), WG_PORT (51820)
|
RA_DEVICE_ID, WG_HOST (default ra.apps.f0xx.org), WG_PORT (45340)
|
||||||
BE_WG_IP (10.66.66.1) — ping target on tunnel
|
BE_WG_IP (172.200.2.1) — ping target on tunnel
|
||||||
RA_UDP_APPLY=1 — same as --apply (wg-quick up)
|
RA_UDP_APPLY=1 — same as --apply (wg-quick up)
|
||||||
|
|
||||||
Steps: DNS → UDP port → optional HTTP E2E → connect JSON → wg conf → optional tunnel → ping
|
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
|
if ping -c 3 -W 2 "$BE_WG_IP"; then
|
||||||
ok "ping ${BE_WG_IP}"
|
ok "ping ${BE_WG_IP}"
|
||||||
else
|
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
|
fi
|
||||||
note "9. Teardown"
|
note "9. Teardown"
|
||||||
sudo wg-quick down "$CONF"
|
sudo wg-quick down "$CONF"
|
||||||
|
|||||||
46
examples/crash_reporter/backend/scripts/sync_wg_peers.php
Executable file
46
examples/crash_reporter/backend/scripts/sync_wg_peers.php
Executable 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'] ?? '')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -97,6 +97,19 @@ foreach (["remote_access_devices","remote_access_sessions","remote_access_events
|
|||||||
echo "OK MariaDB/SQLite remote_access tables present\n";
|
echo "OK MariaDB/SQLite remote_access tables present\n";
|
||||||
' || fail=1
|
' || 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)
|
# API smoke (local php -S or deployed BASE)
|
||||||
BASE="${BASE:-${CRASHES_BASE:-http://127.0.0.1:8080/app/androidcast_project/crashes}}"
|
BASE="${BASE:-${CRASHES_BASE:-http://127.0.0.1:8080/app/androidcast_project/crashes}}"
|
||||||
if [[ -x "$ROOT/scripts/test_remote_access_api.sh" ]]; then
|
if [[ -x "$ROOT/scripts/test_remote_access_api.sh" ]]; then
|
||||||
|
|||||||
@@ -756,6 +756,39 @@ final class RemoteAccessRepository {
|
|||||||
return true;
|
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 {
|
public static function purgeStale(int $maxAgeHours = 24): int {
|
||||||
self::ensureSchema();
|
self::ensureSchema();
|
||||||
$cutoff = time() - max(1, $maxAgeHours) * 3600;
|
$cutoff = time() - max(1, $maxAgeHours) * 3600;
|
||||||
|
|||||||
@@ -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 {
|
private static function runWg(string $args): int {
|
||||||
$prefix = rtrim((string) cfg('remote_access.wg_set_prefix', ''), " \t");
|
$prefix = rtrim((string) cfg('remote_access.wg_set_prefix', ''), " \t");
|
||||||
$cmd = ($prefix !== '' ? $prefix . ' ' : '') . 'wg ' . $args;
|
$cmd = ($prefix !== '' ? $prefix . ' ' : '') . 'wg ' . $args;
|
||||||
|
|||||||
Reference in New Issue
Block a user