Emergency remote access to an office router (OpenWrt 25.12.x, nftables/firewall4) via WireGuard — independent of the primary selective-routing tunnel (Podkop / VLESS Reality) running on the same device.
- Problem
- Design decisions
- Diagram
- 1. Packages and keys
- 2. wg0 interface
- 3. Firewall
- 4. Compatibility with Podkop
- 5. Client side
- 6. Managing clients
- 7. Troubleshooting
- Stack
- Security
The office router performs selective routing of part of its traffic through Podkop (VLESS+Reality → 3x-ui on a VPS). If the proxy chain on the router fails or needs reconfiguring, and there's no physical access to the device, an independent channel into LuCI/SSH is needed.
Solution: a WireGuard tunnel directly to the office's static public IP. Administrative access survives even if Podkop is completely down.
WireGuard, not public SSH. An open SSH port on the WAN is a permanent attack surface (brute force, scanners). WireGuard doesn't respond to packets without a valid key (silent drop) — the service is effectively invisible from outside.
Split-tunnel, not full-tunnel. AllowedIPs is limited to the tunnel subnet and the office LAN. The rest of the client's internet traffic goes out directly — no extra load on the office link, and no risk of accidentally routing personal traffic through work infrastructure.
A separate preshared key per client. PSK is an additional symmetric layer on top of the asymmetric key pair. A shared PSK would mean that compromising one device weakens the security of every other client's connection.
WireGuard as a standalone interface, not layered on top of Podkop. The most common reason to reach the router is a failure in the proxy itself. The admin channel has to stay alive independently of its state.
Client (admin) ──WireGuard (UDP/51820)──► Static public IP ──► wg0 (10.10.10.1/24) ──► LAN (192.168.1.0/24)
Inside the router, WireGuard (wg0) and Podkop/Xray are two independent stacks (interfaces + nftables chains) that never overlap.
Addressing:
| Node | Address |
|---|---|
| Tunnel subnet | 10.10.10.0/24 |
| Router (server) | 10.10.10.1 |
| Clients | 10.10.10.2, 10.10.10.3, … |
| Office LAN | 192.168.1.0/24 |
On the router (over SSH from the local network):
apk add wireguard-tools luci-proto-wireguard
mkdir -p /etc/wireguard && cd /etc/wireguard
umask 077
# Server (router) keys
wg genkey | tee server_private.key | wg pubkey > server_public.key
# First client's keys
wg genkey | tee client_private.key | wg pubkey > client_public.key
# Pre-shared key (unique to this pair)
wg genpsk > preshared.keyumask 077 ensures the key files are readable only by root.
UCI doesn't perform $(...) substitution inside config files — key values are inserted via shell variables in a heredoc:
SERVER_PRIV=$(cat /etc/wireguard/server_private.key)
CLIENT_PUB=$(cat /etc/wireguard/client_public.key)
PSK=$(cat /etc/wireguard/preshared.key)
cat >> /etc/config/network << EOF
config interface 'wg0'
option proto 'wireguard'
option private_key '$SERVER_PRIV'
option listen_port '51820'
list addresses '10.10.10.1/24'
config wireguard_wg0
option description 'admin-remote'
option public_key '$CLIENT_PUB'
option preshared_key '$PSK'
list allowed_ips '10.10.10.2/32'
option route_allowed_ips '1'
EOF
/etc/init.d/network restartVerification:
ip addr show wg0 # interface UP, address 10.10.10.1/24
wg show # server public key, port 51820, client peerNote: the
cat >> ...block appends to the end of the file. Running it twice will duplicate the sections — see Troubleshooting.
cat >> /etc/config/firewall << 'EOF'
config zone
option name 'wireguard'
option input 'ACCEPT'
option output 'ACCEPT'
option forward 'ACCEPT'
list network 'wg0'
config forwarding
option src 'wireguard'
option dest 'lan'
config forwarding
option src 'lan'
option dest 'wireguard'
config rule
option name 'Allow-WireGuard'
option src 'wan'
option proto 'udp'
option dest_port '51820'
option target 'ACCEPT'
EOF
/etc/init.d/firewall restartThe Allow-WireGuard rule is mandatory: without it, inbound UDP packets on 51820 from the WAN are dropped and the connection never comes up.
Verify the rule actually landed in nftables:
nft list ruleset | grep -B2 -A2 "51820"Expected line:
udp dport 51820 counter packets 0 bytes 0 accept comment "!fw4: Allow-WireGuard"
A packets 0 counter before the first client connects is normal.
Podkop creates its own nftables table, PodkopTable, and matches traffic by:
iifname @interfaces— outbound traffic from LAN clients;ip daddragainst thepodkop_subnets/podkop_discord_subnetssets and the FakeIP range198.18.0.0/15.
Inbound WireGuard traffic on port 51820 arrives from the WAN at the router itself and never matches these conditions — there's no conflict. Check before setting up:
ip rule show # only local/main/default — fine
ip route show table all | grep -i podkop # empty: Podkop creates no separate routing tables
nft list ruleset | grep -i -A5 podkop # no rules on udp dport 51820[Interface]
PrivateKey = <client_private.key>
Address = 10.10.10.2/24
DNS = 192.168.1.1
[Peer]
PublicKey = <server_public.key>
PresharedKey = <preshared.key>
Endpoint = <static_public_ip>:51820
AllowedIPs = 10.10.10.0/24, 192.168.1.0/24
PersistentKeepalive = 25PersistentKeepalive = 25 keeps the client's NAT binding alive — without it the tunnel "falls asleep" and the first packets after idle time get dropped.
| Platform | Link |
|---|---|
| iOS | App Store |
| Android | Google Play / direct APK |
| Windows | wireguard.com/install |
| macOS | App Store |
| Linux | wireguard.com/install (wireguard-tools + wg-quick) |
- Install the official WireGuard app.
+→ "Create from QR code" (the QR is generated from the.confcontents with any generator, e.g. the Pythonqrcodepackage), or+→ "Import from file or archive".- Turn the tunnel on and confirm a handshake appears (sent/received counters increasing).
The .conf file and QR code contain a private key — don't keep them lying around in the open longer than needed for import, and don't send them over unencrypted channels.
Each client gets its own key pair and its own preshared key:
cd /etc/wireguard
umask 077
wg genkey | tee client2_private.key | wg pubkey > client2_public.key
wg genpsk > preshared2.key
CLIENT2_PUB=$(cat client2_public.key)
PSK2=$(cat preshared2.key)
uci add network wireguard_wg0
uci set network.@wireguard_wg0[-1].description='client2-remote'
uci set network.@wireguard_wg0[-1].public_key="$CLIENT2_PUB"
uci set network.@wireguard_wg0[-1].preshared_key="$PSK2"
uci add_list network.@wireguard_wg0[-1].allowed_ips='10.10.10.3/32'
uci set network.@wireguard_wg0[-1].route_allowed_ips='1'
uci commit network
/etc/init.d/network restart
wg show # should show two peersAddresses are assigned incrementally: the next client gets 10.10.10.4/32, and so on.
If a client doesn't need the whole local network, narrow AllowedIPs in their config to 10.10.10.0/24 (without 192.168.1.0/24). Only the router itself will be reachable through the tunnel.
uci show network | grep -B1 "client2-remote" # find the peer entry's index
uci delete network.@wireguard_wg0[N]
uci commit network
/etc/init.d/network restartIf a client's device may have been compromised, deleting the peer entry is enough — without a matching server-side record, its keys are useless.
- Firewall rule on the router:
nft list ruleset | grep 51820— should showaccept. - NAT/firewall on the client side and at the ISP — outbound UDP must not be blocked.
- MTU: the default 1420 can be too large on networks with extra encapsulation (PPPoE, etc). If packets fragment, lower it to 1380–1400:
MTU = 1380in the client's[Interface]section. - Cross-check the keys:
PublicKeyin the client config must matchwg showoutput on the router, andEndpointmust match the current public IP.
- On the router:
route_allowed_ips='1'in thewireguard_wg0section — without it, routes aren't added automatically. - Forwarding rules
wireguard → lanandlan → wireguardin/etc/config/firewall. - The client's
AllowedIPsmust include the LAN subnet (192.168.1.0/24), not just the tunnel subnet.
Cause: the config zone 'wireguard' block was appended to /etc/config/firewall twice (the heredoc command was run more than once). firewall4 generates an nft variable from the zone name and fails on the duplicate.
Diagnosis:
uci show firewall | grep -i wireguard
grep -n "wireguard" /etc/config/firewallIf you see two identical @zone[N].name='wireguard' entries, delete the extra set (zone + associated forwarding + rule), starting from the highest index — otherwise the numbering shifts and you'll delete the wrong thing:
uci delete firewall.@rule[N]
uci delete firewall.@forwarding[N]
uci delete firewall.@forwarding[N-1]
uci delete firewall.@zone[N]
uci commit firewall
/etc/init.d/firewall restart| Component | Role |
|---|---|
| OpenWrt 25.12.x | router OS, nftables / firewall4 |
| WireGuard | admin tunnel (wireguard-tools, luci-proto-wireguard) |
| Podkop (podkop.net) | selective routing for LAN traffic, independent stack |
| Xray (VLESS+Reality) | transport for the primary tunnel |
| 3x-ui | Xray control panel on the VPS side |
- Don't store
.conffiles, QR codes, or*.keyfiles in repositories, and don't send them over unencrypted channels. This repo's.gitignorealready excludes those patterns. - Every key, address, and identifier in this document is a placeholder or an example from a private range. Real values are filled in locally and never committed.
- If a private key or
.conffile does end up in a pushed repo's git history, deleting the file in a new commit isn't enough — treat the keys as compromised and regenerate the pairs on the router.
