1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 05:37:52 +03:00
This commit is contained in:
Anton Afanasyeu
2026-06-08 20:53:37 +02:00
parent b981d00f66
commit 1ddb51312c
13 changed files with 302 additions and 105 deletions

View File

@@ -47,15 +47,20 @@ return [
],
// On-demand remote access (WireGuard v1) — see docs/REMOTE_ACCESS_IMPL.md
'remote_access' => [
// Public UDP endpoint shown to devices (FE DNAT → BE wg0)
'wg_endpoint' => 'ra.apps.f0xx.org:51820',
// Server WG public key (wg show wg0 public-key on BE)
'wg_server_public_key' => '',
// Public UDP endpoint (FE DNAT → BE wg0 ListenPort)
'wg_endpoint' => 'ra.apps.f0xx.org:45340',
// wg show wg0 public-key on BE — REQUIRED for production connect
'wg_server_public_key' => 'EV2nnwHH7xe3jA5J46Rjtl8ao+ybZinibKg2Hy83GBw=',
'wg_interface' => 'wg0',
// Run `wg set wg0 peer …` when device receives connect (requires wireguard-tools on BE)
// Client / server addresses (must match wg0 Address subnet on BE)
'wg_server_allowed_ips' => '172.200.2.1/32',
'wg_client_ip_prefix' => '172.200.2.',
'wg_client_ip_min' => 10,
'wg_client_ip_max' => 250,
// Prefix for `wg set` when PHP-FPM runs as nobody (see examples/wireguard/alpine-be-sudoers-wg)
'wg_set_prefix' => 'sudo -n ',
'provision_peers' => true,
// Reject connect if wg genkey/pubkey unavailable (set true on production BE)
'require_wg_tools' => false,
'require_wg_tools' => true,
'session_ttl_s' => 3600,
'min_poll_interval_s' => 45,
// Reverse SSH (alpha): outbound -R from device; no VpnService on phone

View File

@@ -9,8 +9,8 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
source "$ROOT/scripts/ra_lib.sh"
WG_HOST="${WG_HOST:-ra.apps.f0xx.org}"
WG_PORT="${WG_PORT:-51820}"
BE_WG_IP="${BE_WG_IP:-10.66.66.1}"
WG_PORT="${WG_PORT:-45340}"
BE_WG_IP="${BE_WG_IP:-172.200.2.1}"
DEVICE_ID="${RA_DEVICE_ID:-ra-udp-$(hostname -s | tr ' ' '-')}"
CONF="/tmp/ra-udp-${DEVICE_ID}.conf"
APPLY="${RA_UDP_APPLY:-0}"

View File

@@ -336,7 +336,7 @@ final class RemoteAccessRepository {
&& (bool) cfg('remote_access.require_wg_tools', false)) {
return self::raEnvelope('deny', ['reason' => 'wg_tools_required']);
}
$endpoint = (string) ($session['endpoint'] ?? cfg('remote_access.wg_endpoint', 'ra.apps.f0xx.org:51820'));
$endpoint = (string) ($session['endpoint'] ?? WireGuardAddressPool::defaultEndpoint());
$expiresAt = (int) ($session['expires_at'] ?? (time() + 3600));
self::markSessionActive($sessionId);
@@ -435,7 +435,7 @@ final class RemoteAccessRepository {
'client_public' => $pub,
'client_address' => $addr,
'server_public' => $serverPub,
'allowed_ips' => $allowed !== '' ? $allowed : '10.66.66.1/32',
'allowed_ips' => $allowed !== '' ? $allowed : WireGuardAddressPool::serverAllowedIps(),
];
}
$pair = self::generateWireGuardKeyPair();
@@ -444,13 +444,13 @@ final class RemoteAccessRepository {
if ($pub === '' || str_starts_with($pub, 'dev-placeholder-pub-')) {
$pub = WireGuardPeerProvisioner::publicKeyFromPrivate($priv);
}
$clientIp = self::allocateClientAddress($sessionId);
$clientIp = WireGuardAddressPool::allocateClientAddress($sessionId);
$serverPub = (string) cfg('remote_access.wg_server_public_key', '');
if ($serverPub === '') {
$serverPair = self::generateWireGuardKeyPair();
$serverPub = $serverPair['public'];
}
$allowed = '10.66.66.1/32';
$allowed = WireGuardAddressPool::serverAllowedIps();
Database::pdo()->prepare(
'UPDATE remote_access_sessions SET wg_client_private_key = ?, wg_client_public_key = ?, wg_client_address = ?, wg_server_public_key = ?, wg_peer_allowed_ips = ? WHERE session_id = ?'
)->execute([$priv, $pub, $clientIp, $serverPub, $allowed, $sessionId]);
@@ -468,11 +468,6 @@ final class RemoteAccessRepository {
];
}
private static function allocateClientAddress(string $sessionId): string {
$n = abs(crc32($sessionId)) % 200 + 2;
return '10.66.66.' . $n . '/32';
}
/** @return array{private:string,public:string} */
public static function generateWireGuardKeyPair(): array {
$priv = trim((string) @shell_exec('wg genkey 2>/dev/null') ?: '');
@@ -552,7 +547,7 @@ final class RemoteAccessRepository {
$port = (int) cfg('remote_access.rssh.bastion_port', 443);
$endpoint = $port === 443 ? $host : $host . ':' . $port;
} else {
$endpoint = (string) cfg('remote_access.wg_endpoint', 'ra.apps.f0xx.org:51820');
$endpoint = WireGuardAddressPool::defaultEndpoint();
}
$expiresAt = time() + (int) cfg('remote_access.session_ttl_s', 3600);
$now = self::nowSql();

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/** WG client IPs + server AllowedIPs from config (matches live wg0 on BE). */
final class WireGuardAddressPool {
private function __construct() {
}
public static function defaultEndpoint(): string {
return (string) cfg('remote_access.wg_endpoint', 'ra.apps.f0xx.org:45340');
}
public static function serverAllowedIps(): string {
return (string) cfg('remote_access.wg_server_allowed_ips', '172.200.2.1/32');
}
public static function allocateClientAddress(string $sessionId): string {
$prefix = (string) cfg('remote_access.wg_client_ip_prefix', '172.200.2.');
$min = max(2, (int) cfg('remote_access.wg_client_ip_min', 10));
$max = max($min, (int) cfg('remote_access.wg_client_ip_max', 250));
$span = $max - $min + 1;
$host = $min + (abs(crc32($sessionId)) % $span);
return $prefix . $host . '/32';
}
}

View File

@@ -33,15 +33,13 @@ final class WireGuardPeerProvisioner {
$iface = self::interfaceName();
$allowed = trim($clientAddressCidr);
if ($allowed === '') {
$allowed = '10.66.66.2/32';
$allowed = WireGuardAddressPool::allocateClientAddress('default');
}
$cmd = 'wg set ' . escapeshellarg($iface)
$code = self::runWg(
'set ' . escapeshellarg($iface)
. ' peer ' . escapeshellarg($pub)
. ' allowed-ips ' . escapeshellarg($allowed);
exec($cmd, $out, $code);
if ($code !== 0) {
error_log('WireGuardPeerProvisioner add failed: ' . implode(' ', $out));
}
. ' allowed-ips ' . escapeshellarg($allowed)
);
return $code === 0;
}
@@ -54,9 +52,20 @@ final class WireGuardPeerProvisioner {
return;
}
$iface = self::interfaceName();
$cmd = 'wg set ' . escapeshellarg($iface)
self::runWg(
'set ' . escapeshellarg($iface)
. ' peer ' . escapeshellarg($pub)
. ' remove';
exec($cmd);
. ' remove'
);
}
private static function runWg(string $args): int {
$prefix = rtrim((string) cfg('remote_access.wg_set_prefix', ''), " \t");
$cmd = ($prefix !== '' ? $prefix . ' ' : '') . 'wg ' . $args;
exec($cmd . ' 2>&1', $out, $code);
if ($code !== 0) {
error_log('WireGuardPeerProvisioner: ' . $cmd . ' failed: ' . implode(' ', $out));
}
return $code;
}
}

View File

@@ -65,6 +65,7 @@ require_once __DIR__ . '/TicketRepository.php';
require_once __DIR__ . '/GraphRepository.php';
require_once __DIR__ . '/RemoteAccessRepository.php';
require_once __DIR__ . '/WireGuardPeerProvisioner.php';
require_once __DIR__ . '/WireGuardAddressPool.php';
require_once __DIR__ . '/RsshSessionProvisioner.php';
require_once __DIR__ . '/AnalyticsHead.php';

View File

@@ -7,6 +7,6 @@ PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE;iptables -D FORWARD -o %i -j ACCEPT; ip6tables -D FORWARD -i %i -j ACCEPT; ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE;ip6tables -D FORWARD -o %i -j ACCEPT
[Peer]
# obtained from client device via wireguard connection setup process
PublicKey = EV2nnwHH7xe3jA5J46Rjtl8ao+ybZinibKg2Hy83GBw=
AllowedIPs = 172.200.2.2/16, fddd::1/128
# Static lab peer only — remote-access clients are added dynamically via wg set (PHP).
# PublicKey = <client-device-pubkey>
# AllowedIPs = 172.200.2.2/32

View File

@@ -0,0 +1,43 @@
#!/bin/bash
# FE (f0xx-monstro): DNAT public UDP → BE wg0. Matches existing rules to 10.7.16.128 (8088, etc.).
# Invoked from xen_firewall.sh start/stop; also: wireguard_fe_dnat.sh [start|stop]
set -euo pipefail
BE_IP="${BE_IP:-10.7.16.128}"
WG_PORT="${WG_PORT:-45340}"
function start() {
if iptables -t nat -C PREROUTING -p udp --dport "$WG_PORT" -j DNAT --to-destination "${BE_IP}:${WG_PORT}" 2>/dev/null; then
echo "DNAT rule already present for udp/${WG_PORT}"
else
iptables -t nat -A PREROUTING -p udp --dport "$WG_PORT" -j DNAT --to-destination "${BE_IP}:${WG_PORT}"
echo "Added PREROUTING DNAT udp/${WG_PORT} -> ${BE_IP}:${WG_PORT}"
fi
if iptables -C FORWARD -p udp -d "${BE_IP}" --dport "$WG_PORT" -j ACCEPT 2>/dev/null; then
echo "FORWARD rule already present"
else
iptables -A FORWARD -p udp -d "${BE_IP}" --dport "$WG_PORT" -j ACCEPT
echo "Added FORWARD udp -> ${BE_IP}:${WG_PORT}"
fi
}
function stop() {
if iptables -t nat -C PREROUTING -p udp --dport "$WG_PORT" -j DNAT --to-destination "${BE_IP}:${WG_PORT}" 2>/dev/null; then
iptables -t nat -D PREROUTING -p udp --dport "$WG_PORT" -j DNAT --to-destination "${BE_IP}:${WG_PORT}"
echo "Removed PREROUTING DNAT udp/${WG_PORT}"
else
echo "DNAT rule not present for udp/${WG_PORT}"
fi
if iptables -C FORWARD -p udp -d "${BE_IP}" --dport "$WG_PORT" -j ACCEPT 2>/dev/null; then
iptables -D FORWARD -p udp -d "${BE_IP}" --dport "$WG_PORT" -j ACCEPT
echo "Removed FORWARD udp -> ${BE_IP}:${WG_PORT}"
else
echo "FORWARD rule not present"
fi
}
cmd=$1
cmd=${cmd:-start}
[ "$cmd" = "start" ] && start || stop

View File

@@ -0,0 +1,6 @@
# Install on Alpine BE: /etc/sudoers.d/androidcast-wg (chmod 440)
# PHP-FPM pool user is nginx (see /etc/php81/php-fpm.d/www.conf).
Defaults:nobody !requiretty
Defaults:nginx !requiretty
nobody ALL=(root) NOPASSWD: /usr/bin/wg set wg0 peer *
nginx ALL=(root) NOPASSWD: /usr/bin/wg set wg0 peer *

View File

@@ -0,0 +1,47 @@
#!/bin/bash
# Run on alpine-be as root after syncing backend tree.
set -euo pipefail
ROOT="${1:-/var/www/localhost/htdocs/apps/app/androidcast_project/android_cast/examples/crash_reporter/backend}"
CFG="$ROOT/config/config.php"
SUDOERS_DST="/etc/sudoers.d/androidcast-wg"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [[ ! -f "$CFG" ]]; then
echo "Missing $CFG" >&2
exit 1
fi
php81 -r '
$path = $argv[1];
$c = require $path;
if (!is_array($c)) { fwrite(STDERR, "config.php must return array\n"); exit(1); }
if (isset($c["remote_access"]) && is_array($c["remote_access"]) && ($c["remote_access"]["wg_server_public_key"] ?? "") !== "") {
echo "remote_access already configured\n";
exit(0);
}
$c["remote_access"] = array_merge($c["remote_access"] ?? [], [
"wg_endpoint" => "ra.apps.f0xx.org:45340",
"wg_server_public_key" => "EV2nnwHH7xe3jA5J46Rjtl8ao+ybZinibKg2Hy83GBw=",
"wg_interface" => "wg0",
"wg_server_allowed_ips" => "172.200.2.1/32",
"wg_client_ip_prefix" => "172.200.2.",
"wg_client_ip_min" => 10,
"wg_client_ip_max" => 250,
"wg_set_prefix" => "sudo ",
"provision_peers" => true,
"require_wg_tools" => true,
"session_ttl_s" => 3600,
"min_poll_interval_s" => 45,
]);
$export = var_export($c, true);
file_put_contents($path, "<?php\nreturn " . $export . ";\n");
echo "Wrote remote_access into config.php\n";
' "$CFG"
install -m 440 "$SCRIPT_DIR/alpine-be-sudoers-wg" "$SUDOERS_DST"
visudo -cf "$SUDOERS_DST"
echo "Installed $SUDOERS_DST"
rc-service php-fpm81 restart 2>/dev/null || rc-service php-fpm restart 2>/dev/null || true
echo "Restarted PHP-FPM"

View File

@@ -0,0 +1,13 @@
#!/bin/bash
# Raspberry Pi router (f0xx.org): WAN UDP 45340 → FE (10.7.0.10). Persist via your firewall save.
set -euo pipefail
FE_IP="${FE_IP:-10.7.0.10}"
WG_PORT="${WG_PORT:-45340}"
WAN_IF="${WAN_IF:-eth0}"
if iptables -t nat -C PREROUTING -i "$WAN_IF" -p udp --dport "$WG_PORT" -j DNAT --to-destination "${FE_IP}:${WG_PORT}" 2>/dev/null; then
echo "Router DNAT already present"
else
iptables -t nat -A PREROUTING -i "$WAN_IF" -p udp --dport "$WG_PORT" -j DNAT --to-destination "${FE_IP}:${WG_PORT}"
echo "Added router PREROUTING udp/${WG_PORT} -> ${FE_IP}:${WG_PORT}"
fi