mirror of
git://f0xx.org/android_cast
synced 2026-07-29 04:57:40 +03:00
sync on be, vpn
This commit is contained in:
@@ -81,6 +81,14 @@
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function formatBytes(n) {
|
||||
const b = Number(n) || 0;
|
||||
if (b < 1024) return b + ' B';
|
||||
if (b < 1024 * 1024) return (b / 1024).toFixed(1) + ' KiB';
|
||||
if (b < 1024 * 1024 * 1024) return (b / (1024 * 1024)).toFixed(2) + ' MiB';
|
||||
return (b / (1024 * 1024 * 1024)).toFixed(2) + ' GiB';
|
||||
}
|
||||
|
||||
function getColWidths() {
|
||||
try {
|
||||
const w = JSON.parse(lsGet(STORAGE.colWidths, '{}'));
|
||||
@@ -393,10 +401,26 @@
|
||||
if (d.sdk_int != null) lines.push('Android API: ' + d.sdk_int);
|
||||
if (d.os_release) lines.push('Android version: ' + d.os_release);
|
||||
if (d.abis && d.abis.length) lines.push('ABIs: ' + formatAbis(d.abis));
|
||||
if (d.wan_ip) lines.push('WAN IP (last poll): ' + d.wan_ip);
|
||||
if (d.lan_ip) lines.push('LAN IP: ' + d.lan_ip);
|
||||
if (d.lan_ip) lines.push('Device LAN IP: ' + d.lan_ip);
|
||||
if (d.device_wan_ip) lines.push('Device WAN IP: ' + d.device_wan_ip);
|
||||
if (d.poll_source_ip) {
|
||||
let pollLabel = 'Poll source IP (server view)';
|
||||
if (d.poll_source_is_private) {
|
||||
pollLabel += ' — intra/private hop, not device WAN';
|
||||
}
|
||||
lines.push(pollLabel + ': ' + d.poll_source_ip);
|
||||
}
|
||||
if (d.vpn_ip) lines.push('VPN IP: ' + d.vpn_ip);
|
||||
if (d.vpn_public_key) lines.push('VPN public key: ' + d.vpn_public_key);
|
||||
if (d.wg_rx_bytes != null || d.wg_tx_bytes != null) {
|
||||
const rx = formatBytes(d.wg_rx_bytes || 0);
|
||||
const tx = formatBytes(d.wg_tx_bytes || 0);
|
||||
lines.push('VPN traffic (BE wg): ↓' + rx + ' ↑' + tx);
|
||||
}
|
||||
if (d.wg_latest_handshake > 0) {
|
||||
lines.push('VPN last handshake: ' + new Date(d.wg_latest_handshake * 1000).toISOString());
|
||||
}
|
||||
if (d.wg_endpoint) lines.push('VPN endpoint: ' + d.wg_endpoint);
|
||||
lines.push('App version: ' + (d.app_version || '—'));
|
||||
lines.push('Opt-in mode: ' + (d.opt_in_mode || 'none'));
|
||||
lines.push('Last seen: ' + (d.last_seen_at || '—'));
|
||||
|
||||
@@ -80,8 +80,14 @@ else
|
||||
note "php CLI cannot run wg genkey — ensure www-data can run wg if provision_peers=true"
|
||||
fi
|
||||
|
||||
# DB tables (via bootstrap)
|
||||
"$PHP" -r '
|
||||
# DB tables (via bootstrap) — use FPM user + php81 so PDO/mysql extensions match production
|
||||
DB_RUN="$PHP"
|
||||
DB_USER=""
|
||||
if [[ "$(id -u)" -eq 0 ]] && id nginx >/dev/null 2>&1; then
|
||||
DB_RUN="sudo -u nginx $PHP"
|
||||
DB_USER="nginx"
|
||||
fi
|
||||
$DB_RUN -r '
|
||||
require "'"$ROOT"'/src/bootstrap.php";
|
||||
RemoteAccessRepository::ensureSchema();
|
||||
$pdo = Database::pdo();
|
||||
|
||||
@@ -266,11 +266,12 @@ final class RemoteAccessRepository {
|
||||
$pdo = Database::pdo();
|
||||
$now = self::nowSql();
|
||||
$pdo->prepare(
|
||||
'UPDATE remote_access_devices SET opt_in_mode = ?, last_random = ?, app_version = ?, last_seen_at = ?, updated_at = ? WHERE device_id = ?'
|
||||
'UPDATE remote_access_devices SET whitelisted = 0, opt_in_mode = ?, last_random = ?, app_version = ?, last_seen_at = ?, updated_at = ? WHERE device_id = ?'
|
||||
)->execute([self::OPT_NONE, $random, $appVersion, $now, $now, $deviceId]);
|
||||
|
||||
self::closeActiveSessionsForDevice($deviceId, 'user_disabled');
|
||||
self::logEvent($deviceId, null, null, 'user_disable', 'device_opt_out', null, $clientIp);
|
||||
self::logEvent($deviceId, null, null, 'whitelist_revoked', 'device_opt_out', null, $clientIp);
|
||||
|
||||
return self::raEnvelope('disabled', ['reason' => 'user_opt_out']);
|
||||
}
|
||||
@@ -667,7 +668,9 @@ final class RemoteAccessRepository {
|
||||
$sql .= ' WHERE company_id = ?';
|
||||
$params[] = $companyId;
|
||||
}
|
||||
$sql .= ' ORDER BY last_seen_at DESC, device_id ASC';
|
||||
$sql .= Database::isMysql()
|
||||
? ' ORDER BY last_seen_at DESC NULLS LAST, device_id ASC'
|
||||
: ' ORDER BY datetime(COALESCE(last_seen_at, \'1970-01-01\')) DESC, device_id ASC';
|
||||
$st = Database::pdo()->prepare($sql);
|
||||
$st->execute($params);
|
||||
return $st->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||||
@@ -845,11 +848,17 @@ final class RemoteAccessRepository {
|
||||
$row['os_release'] = $ctx['os_release'] ?? '';
|
||||
$row['sdk_int'] = $ctx['sdk_int'] ?? null;
|
||||
$row['abis'] = $ctx['abis'] ?? [];
|
||||
$row['wan_ip'] = self::resolveDeviceWanIp($row, $storedMeta);
|
||||
$row['lan_ip'] = self::resolveDeviceLanIp($storedMeta);
|
||||
$row['poll_source_ip'] = self::resolvePollSourceIp($row);
|
||||
$row['poll_source_is_private'] = self::isPrivateIp($row['poll_source_ip']);
|
||||
$row['device_wan_ip'] = trim((string) ($storedMeta['wan_ip'] ?? ''));
|
||||
$net = self::lookupDeviceSessionNetwork($deviceId);
|
||||
$row['vpn_ip'] = $net['vpn_ip'] ?? '';
|
||||
$row['vpn_public_key'] = $net['vpn_public_key'] ?? '';
|
||||
$row['wg_rx_bytes'] = $net['wg_rx_bytes'] ?? 0;
|
||||
$row['wg_tx_bytes'] = $net['wg_tx_bytes'] ?? 0;
|
||||
$row['wg_latest_handshake'] = $net['wg_latest_handshake'] ?? 0;
|
||||
$row['wg_endpoint'] = $net['wg_endpoint'] ?? '';
|
||||
$row['issue_count'] = $ctx['issue_count'] ?? 0;
|
||||
$row['graph_session_count'] = $ctx['graph_session_count'] ?? 0;
|
||||
$row['latest_issue_id'] = $ctx['latest_issue_id'] ?? null;
|
||||
@@ -911,12 +920,8 @@ final class RemoteAccessRepository {
|
||||
return (time() - $t) <= $maxAgeSec;
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $storedMeta */
|
||||
private static function resolveDeviceWanIp(array $row, array $storedMeta): string {
|
||||
$fromDevice = trim((string) ($storedMeta['wan_ip'] ?? ''));
|
||||
if ($fromDevice !== '') {
|
||||
return self::normalizeClientIp($fromDevice);
|
||||
}
|
||||
/** @param array<string, mixed> $row */
|
||||
private static function resolvePollSourceIp(array $row): string {
|
||||
$fromRow = trim((string) ($row['last_client_ip'] ?? ''));
|
||||
if ($fromRow !== '') {
|
||||
return self::normalizeClientIp($fromRow);
|
||||
@@ -936,6 +941,17 @@ final class RemoteAccessRepository {
|
||||
return self::normalizeClientIp($ip);
|
||||
}
|
||||
|
||||
private static function isPrivateIp(string $ip): bool {
|
||||
if ($ip === '' || !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
return false;
|
||||
}
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $storedMeta */
|
||||
private static function resolveDeviceLanIp(array $storedMeta): string {
|
||||
return trim((string) ($storedMeta['lan_ip'] ?? ''));
|
||||
@@ -953,9 +969,16 @@ final class RemoteAccessRepository {
|
||||
return $raw;
|
||||
}
|
||||
|
||||
/** @return array{vpn_ip: string, vpn_public_key: string} */
|
||||
/** @return array{vpn_ip: string, vpn_public_key: string, wg_rx_bytes: int, wg_tx_bytes: int, wg_latest_handshake: int, wg_endpoint: string} */
|
||||
private static function lookupDeviceSessionNetwork(string $deviceId): array {
|
||||
$empty = ['vpn_ip' => '', 'vpn_public_key' => ''];
|
||||
$empty = [
|
||||
'vpn_ip' => '',
|
||||
'vpn_public_key' => '',
|
||||
'wg_rx_bytes' => 0,
|
||||
'wg_tx_bytes' => 0,
|
||||
'wg_latest_handshake' => 0,
|
||||
'wg_endpoint' => '',
|
||||
];
|
||||
if ($deviceId === '') {
|
||||
return $empty;
|
||||
}
|
||||
@@ -978,9 +1001,15 @@ final class RemoteAccessRepository {
|
||||
$vpnIp = $addr !== '' ? preg_replace('/\/\d+$/', '', $addr) ?? $addr : '';
|
||||
$pub = trim((string) ($row['wg_client_public_key'] ?? ''));
|
||||
|
||||
$stats = $pub !== '' ? WireGuardTrafficStats::forPublicKey($pub) : null;
|
||||
|
||||
return [
|
||||
'vpn_ip' => is_string($vpnIp) ? trim($vpnIp) : '',
|
||||
'vpn_public_key' => $pub,
|
||||
'wg_rx_bytes' => (int) ($stats['rx_bytes'] ?? 0),
|
||||
'wg_tx_bytes' => (int) ($stats['tx_bytes'] ?? 0),
|
||||
'wg_latest_handshake' => (int) ($stats['latest_handshake'] ?? 0),
|
||||
'wg_endpoint' => (string) ($stats['endpoint'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/** Live WireGuard transfer counters from `wg show dump` (BE data plane). */
|
||||
final class WireGuardTrafficStats {
|
||||
private function __construct() {}
|
||||
|
||||
/** @return array<string, array{rx_bytes: int, tx_bytes: int, latest_handshake: int, endpoint: string}> */
|
||||
public static function peerMap(): array {
|
||||
static $cache = null;
|
||||
static $cacheAt = 0;
|
||||
if ($cache !== null && (time() - $cacheAt) < 5) {
|
||||
return $cache;
|
||||
}
|
||||
$cache = self::loadPeerMap();
|
||||
$cacheAt = time();
|
||||
return $cache;
|
||||
}
|
||||
|
||||
/** @return array{rx_bytes: int, tx_bytes: int, latest_handshake: int, endpoint: string}|null */
|
||||
public static function forPublicKey(string $publicKey): ?array {
|
||||
$pub = trim($publicKey);
|
||||
if ($pub === '') {
|
||||
return null;
|
||||
}
|
||||
return self::peerMap()[$pub] ?? null;
|
||||
}
|
||||
|
||||
/** @return array<string, array{rx_bytes: int, tx_bytes: int, latest_handshake: int, endpoint: string}> */
|
||||
private static function loadPeerMap(): array {
|
||||
$iface = (string) cfg('remote_access.wg_interface', 'wg0');
|
||||
$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 [];
|
||||
}
|
||||
$out = [];
|
||||
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) < 7) {
|
||||
continue;
|
||||
}
|
||||
$pub = trim($cols[0]);
|
||||
if ($pub === '') {
|
||||
continue;
|
||||
}
|
||||
$out[$pub] = [
|
||||
'rx_bytes' => (int) ($cols[5] ?? 0),
|
||||
'tx_bytes' => (int) ($cols[6] ?? 0),
|
||||
'latest_handshake' => (int) ($cols[4] ?? 0),
|
||||
'endpoint' => trim((string) ($cols[2] ?? '')),
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
public static function formatBytes(int $bytes): string {
|
||||
if ($bytes < 1024) {
|
||||
return $bytes . ' B';
|
||||
}
|
||||
if ($bytes < 1024 * 1024) {
|
||||
return round($bytes / 1024, 1) . ' KiB';
|
||||
}
|
||||
if ($bytes < 1024 * 1024 * 1024) {
|
||||
return round($bytes / (1024 * 1024), 2) . ' MiB';
|
||||
}
|
||||
return round($bytes / (1024 * 1024 * 1024), 2) . ' GiB';
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,7 @@ require_once __DIR__ . '/UrlShortenerDatabase.php';
|
||||
require_once __DIR__ . '/ShortLinksRepository.php';
|
||||
require_once __DIR__ . '/WireGuardPeerProvisioner.php';
|
||||
require_once __DIR__ . '/WireGuardAddressPool.php';
|
||||
require_once __DIR__ . '/WireGuardTrafficStats.php';
|
||||
require_once __DIR__ . '/RsshSessionProvisioner.php';
|
||||
require_once __DIR__ . '/AnalyticsHead.php';
|
||||
|
||||
|
||||
@@ -443,6 +443,7 @@
|
||||
Devices appear after they poll remote access (dev settings → WireGuard or RSSH). Sort is by <strong>last seen</strong> — top rows are actively polling.
|
||||
Whitelist rows marked <span class="tag-pill tag-pill--warn">Needs whitelist</span> (opted in, not yet allowed).
|
||||
Stale rows have not polled in 7+ days.
|
||||
<strong>Poll source IP</strong> is the address BE sees on the HTTP request (often a router or proxy on <code>10.7.x.x</code>) — not the device WAN or LAN.
|
||||
</p>
|
||||
<div class="reports-table-wrap">
|
||||
<table class="data-table reports-tree reports-table--cols" id="ra-devices-table">
|
||||
|
||||
Reference in New Issue
Block a user