Files
ServerKit/backend/tests/test_nginx_remote_upstream.py
Juan DenisandClaude Opus 4.8 7cf8d4e18a feat(remote-access): NAT-traversing WireGuard tunnels to expose private services
Self-hosted tunneled reverse proxy: a service on a NAT'd host (e.g. a home
Jellyfin box) gets a public HTTPS hostname with no port-forwarding, by pairing
two agents over WireGuard. Implements Phases 0-3 of docs/REMOTE_ACCESS_ROADMAP.md.

Agent (Go):
- new internal/wireguard package: Manager interface + kernel backend (wg/ip)
  and a userspace netstack backend (wireguard-go + gVisor) so Windows/macOS
  peers need no driver/admin; in-process keygen returns only the public key
  (private keys never leave the host)
- wireguard:keygen/interface/peer/status/forward/unforward + firewall
  :allow_port/:deny_port commands; wireguard + firewall capability probes
- TCP forwarder (tnet.ListenTCP / net.Listen -> dial localhost) carries
  traffic to the real service behind a netstack peer

Backend (Python):
- Tunnel + ExposedService models; tunnel_netutil (subnet allocator, health);
  tunnel_broker_service (keygen->up->peer->verify, firewall open, reconcile-on-
  reconnect); tunnel_publish_service (forward -> nginx -> DNS -> auth -> cert)
- nginx remote-upstream vhost (WebSocket + proxy_buffering off)
- /api/v1/tunnels CRUD + /<id>/services publish/unpublish
- panel-authoritative reconcile hooked into the agent gateway on reconnect

Frontend (React):
- Remote Access page (tunnels + exposed services, status, teardown) and an
  "Expose a Local Service" wizard; new services/api/tunnels.js

Verified: go build (win/linux) + vet + go tests (forwarder + no-leak parsers);
py_compile + pytest (netutil, reconcile, nginx template); create_app boot;
eslint + npm run build. Live multi-host run pending.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:20:00 -04:00

27 lines
1.0 KiB
Python

"""Phase 2 #12: the nginx remote-upstream vhost. Pure template/validation
checks — no nginx or filesystem needed."""
from app.services.nginx_service import NginxService
def test_remote_upstream_template_renders():
cfg = NginxService.REMOTE_UPSTREAM_TEMPLATE.format(
name='jellyfin.example.com',
domains='jellyfin.example.com',
upstream='10.88.3.2:8096',
)
assert 'proxy_pass http://10.88.3.2:8096;' in cfg
assert 'server_name jellyfin.example.com;' in cfg
# streaming + websocket essentials (Jellyfin)
assert 'proxy_buffering off;' in cfg
assert 'proxy_set_header Upgrade $http_upgrade;' in cfg
assert "proxy_set_header Connection 'upgrade';" in cfg
def test_create_site_remote_requires_upstream():
# The 'remote' branch rejects a missing upstream before touching the FS.
res = NginxService.create_site(
name='x', app_type='remote', domains=['x.example.com'], root_path='')
assert res.get('success') is False
assert 'upstream' in res.get('error', '').lower()