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-08 20:53:37 +02:00
parent b981d00f66
commit 1ddb51312c
13 changed files with 302 additions and 105 deletions

View File

@@ -85,10 +85,13 @@ public final class RemoteAccessVpnService extends VpnService {
String[] parts = address.split("/");
b.addAddress(InetAddress.getByName(parts[0]),
parts.length > 1 ? Integer.parseInt(parts[1]) : 32);
} else {
b.addAddress("10.66.66.2", 32);
}
b.addRoute("10.66.66.0", 24);
for (String route : parseAllowedRoutes(wgConfig)) {
String[] rp = route.split("/");
if (rp.length >= 2) {
b.addRoute(rp[0], Integer.parseInt(rp[1]));
}
}
b.setMtu(1280);
tun = b.establish();
Log.i(TAG, "VPN interface established");
@@ -105,6 +108,25 @@ public final class RemoteAccessVpnService extends VpnService {
return m.find() ? m.group(1).trim() : null;
}
/** Routes from AllowedIPs= lines (split tunnel — typically server wg /32). */
private static String[] parseAllowedRoutes(String wgConfig) {
if (wgConfig == null) {
return new String[0];
}
Matcher m = Pattern.compile("(?m)^AllowedIPs\\s*=\\s*(.+)$").matcher(wgConfig);
if (!m.find()) {
return new String[0];
}
String[] parts = m.group(1).trim().split("\\s*,\\s*");
java.util.ArrayList<String> out = new java.util.ArrayList<>();
for (String p : parts) {
if (!p.isEmpty()) {
out.add(p.trim());
}
}
return out.toArray(new String[0]);
}
private void tearDown() {
if (tun != null) {
try {

View File

@@ -1,6 +1,6 @@
# Open tasks — dependency graph
_Last synced: 2026-06-08. Renumbered per [20260608_ALPHA_PRIORITIES.md](20260608_ALPHA_PRIORITIES.md)._
_Last synced: 2026-06-04. **2.12.3 postponed** (developer DNS path deferred). Renumbered per [20260608_ALPHA_PRIORITIES.md](20260608_ALPHA_PRIORITIES.md)._
**Documentation index:** [README.md](README.md)
@@ -9,25 +9,53 @@ _Last synced: 2026-06-08. Renumbered per [20260608_ALPHA_PRIORITIES.md](20260608
## Table of contents
<!-- toc -->
- [Priority tiers](#priority-tiers)
- [Removed from graph (done)](#removed-from-graph-done)
- [Postponed (off graph)](#postponed-off-graph)
- [Priority tiers (open only)](#priority-tiers-open-only)
- [Dependency graph](#dependency-graph)
- [Task index](#task-index)
- [Task index (open)](#task-index-open)
- [Critical path](#critical-path)
<!-- /toc -->
---
## Priority tiers
## Removed from graph (done)
| ID | Note |
|----|------|
| **0.1** | Stream dump — `StreamDumpWriter` + dev setting |
| **3.x** | Register + verify + TOTP + migration `008` (BE + views) |
| **4.x** | RSSH alpha — BE bastion, Android mode, JSch tunnel |
| **4.wg** | WG lab — BE `wg0`, FE DNAT via `xen_firewall.sh`, prod API smoke OK |
| **RBAC** (core) | Admin panel + lockout clear; **5.5 validate** still open |
---
## Postponed (off graph)
| ID | Owner | Note |
|----|-------|------|
| **2.1** | Developer | DNS MX/SPF — deferred |
| **2.2** | Developer | Forward addresses — deferred |
| **2.3** | Developer | Inbound verify — deferred |
| **2.4** | Developer | Gmail filters — optional; with 2.x |
| **2.5** | Agent | SMTP outbound — frozen until 2.1 resumes |
| **5.4** (mail) | Agent | G2G6 **email** legs — frozen until 2.5; UI/TOTP can be tested without mail |
Checklist when resumed: [tmp/20260608_help_request.md](../tmp/20260608_help_request.md)
---
## Priority tiers (open only)
| Tier | IDs | Alpha blocker |
|------|-----|---------------|
| P0 debug | **0.1** stream dump | No (feeds 6.5/6.6) |
| P1 | **1** graphs | No |
| P2 | **2.12.4** dev, **2.5** + **3.x** agent, RBAC | **Yes** (auth) |
| P3 | **4.x** RSSH | **Yes** |
| P4 | **5.x** backend validation | **Yes** |
| P5 | **6.x** app LAN | LAN alpha |
| P9 | **9.x** landing | No |
| **WIP / QA** | Browser JS, reachability | **Yes** (blocks BE sign-off) |
| **P1** | **1.1 / 1.2** graphs | No |
| **P4** | **5.15.3, 5.5** validation + OTA/URLs | **Yes** (auth mail path **deferred**) |
| **P5** | **6.16.6** app LAN | LAN alpha |
| **P9** | **9.x** landing | No |
| **Infra** | Router WG DNAT persist | No (lab only) |
---
@@ -35,87 +63,90 @@ _Last synced: 2026-06-08. Renumbered per [20260608_ALPHA_PRIORITIES.md](20260608
```mermaid
flowchart TB
S01["0.1 stream dump<br/>agent · P0"]
G1["1.x graphs<br/>agent · P1"]
subgraph WIP["WIP — QA / deploy"]
BR["Browser checks<br/>JS SyntaxError · views"]
REACH["Services reachability<br/>OTA/crash URLs"]
end
D21["2.1 DNS<br/>DEV"]
D22["2.2 forward<br/>DEV"]
D23["2.3 inbound test<br/>DEV"]
A25["2.5 SMTP<br/>agent"]
A3["3.x 2FA<br/>agent"]
RBAC["RBAC<br/>agent"]
subgraph P1["P1 graphs"]
G11["1.1 pending fixes"]
G12["1.2 pending FRs"]
end
R4["4.x RSSH<br/>agent · P3"]
subgraph P4["P4 backend alpha"]
B51["5.1 OTA channel<br/>50/50"]
B52["5.2 device URLs<br/>50/50"]
B53["5.3 RSSH validate"]
B55["5.5 RBAC validate"]
end
B51["5.1 OTA<br/>50/50"]
B52["5.2 URLs<br/>50/50"]
B53["5.3 RSSH validate"]
B54["5.4 auth validate"]
B55["5.5 RBAC validate"]
subgraph P5["P5 app LAN — dev"]
A61["6.1 Opus/Speex"]
A62["6.2 LAN soak"]
A63["6.3 lifecycle"]
A64["6.4 rotation"]
A65["6.5 audio lag"]
A66["6.6 stream analysis<br/>uses 0.1 dumps"]
end
A61["6.1 Opus"]
A62["6.2 LAN soak"]
A63["6.3 lifecycle"]
A64["6.4 rotation"]
A65["6.5 audio lag"]
subgraph P9["P9 landing"]
L9["9.x hub / globe"]
end
L9["9.x landing<br/>LOW"]
subgraph INFRA["Infra — non-alpha"]
RDNAT["Router WG DNAT<br/>f0xx.org persist"]
end
FULL["FULL_ALPHA"]
FULL["FULL_ALPHA<br/>mail path deferred"]
S01 -.-> A65 & A64
D21 --> D22 --> D23
D21 --> A25 --> A3 --> RBAC
A3 --> B54 --> FULL
RBAC --> B55
BR --> G11 & G12
BR --> B55
REACH --> B51 & B52
R4 --> B53 --> FULL
B51 & B52 --> FULL
B53 --> FULL
B55 --> FULL
B51 --> FULL
B52 --> FULL
A62 --> FULL
A63 <--> A64
G1 -.-> L9
L9 -.-> RBAC
A63 <--> A64
A65 --> A66
G11 -.-> L9
style FULL fill:#1e3a5f,color:#fff
style D21 fill:#b45309,color:#fff
style D22 fill:#b45309,color:#fff
style D23 fill:#b45309,color:#fff
style R4 fill:#7c2d12,color:#fff
style A3 fill:#7c2d12,color:#fff
style BR fill:#991b1b,color:#fff
```
---
## Task index
## Task index (open)
| ID | Owner | Depends on |
|----|-------|------------|
| **0.1** | Agent | |
| **1 / 1.1 / 1.2** | Agent | BE deploy soft |
| **2.12.4** | **Developer** | DNS UI access |
| **2.5** | Agent | 2.1 |
| **3.x** | Agent | 2.5 |
| **RBAC** | Agent | 3.x, stable pages |
| **4.x** | Agent | BE deploy |
| **5.15.2** | 50/50 | deploy |
| **5.3** | Agent | 4.x |
| **5.45.5** | Agent | 3.x, RBAC |
| **6.16.6** | Developer (~99%) | 0.1 ↔ 6.6 |
| **9.x** | Agent (low) | — |
| ID | Owner | Depends on | Status |
|----|-------|------------|--------|
| **Browser** | Agent | BE deploy | WIP — `SyntaxError` on crashes/graphs/tickets |
| **Reachability** | 50/50 | deploy | WIP — OTA path, crash URL host |
| **1.1** | Agent | browser OK | Open |
| **1.2** | Agent | 1.1 soft | Open |
| **5.1** | 50/50 | reachability | Open |
| **5.2** | 50/50 | reachability | Open |
| **5.3** | Agent | 4.x ✓ | Validate E2E RSSH |
| **5.5** | Agent | RBAC ✓, browser | Validate admin flows |
| **6.16.6** | Developer (~99%) | 0.1 ✓ for 6.6 | Open |
| **9.x** | Agent (low) | bandwidth | Open |
| **Router DNAT** | Agent | — | Open — `router_wireguard_dnat.sh` |
---
## Critical path
```text
DEV: 2.1 → 2.2 → 2.3
AGENT: 2.5 → 3.x → 5.4 ──┐
AGENT: 4.x → 5.3 ─────────┼──► FULL_ALPHA
SHARED: 5.1 OTA, 5.2 URLs ─┤
DEV: 6.2 LAN soak ────────┘
BLOCKER: Browser JS fix + deploy ──► 1.x, 5.5 UI validation
Parallel: 0.1, 1.x, 6.1/6.36.5, 9.x
AGENT: 5.3 RSSH validate (4.x done) ──┐
SHARED: 5.1 OTA, 5.2 URLs ─────────────┼──► FULL_ALPHA (LAN + RSSH + RBAC)
DEV: 6.2 LAN soak ─────────────────┘
Deferred (not on graph): 2.1 → 2.2 → 2.3 → 2.5 → 5.4 mail E2E
Parallel: 6.1/6.36.6, 9.x, router WG DNAT, Android APK rebuild (WG route fix)
```
**Developer blocked checklist:** [tmp/20260608_help_request.md](../tmp/20260608_help_request.md)

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