Initial commit

This commit is contained in:
Asger Weirsøe
2026-05-27 14:41:00 +02:00
commit 1410a93972
16 changed files with 833 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
wg-configs/*.conf
*.env
service/weircon-random-proxy
+168
View File
@@ -0,0 +1,168 @@
# weircon-random-proxy
En public-facing **fetch-API** der henter en URL gennem en tilfældig (eller klient-valgt) Proton WireGuard tunnel og returnerer svaret. Erstatter den lokale `nh-scrape/proxy-stack/` Docker-stak med en LXC på Proxmox.
## Arkitektur
```
Internet
┌──────────────────────────────────┐
│ NginxProxyManager (eksisterende)│
│ • TLS termination │
│ • Tjekker X-WEIRCON-RANDOM-IP │
│ (API key) i header │
│ • Stripper headeren før proxy │
└──────────────┬───────────────────┘
│ http://<lxc-ip>:8080
┌──────────────────────────────────────────────────────┐
│ LXC: weircon-random-proxy (unprivileged + TUN) │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ fetch-service (Go, net/http+ReverseProxy :8080)│ │
│ │ • Læser mål-URL fra: │ │
│ │ - query: ?url=https://example.com │ │
│ │ - header: X-WEIRCON-RANDOM-IP-REDIRECT │ │
│ │ • Læser proxy-valg fra: │ │
│ │ - header: X-WEIRCON-PROXY-ID (0-9) │ │
│ │ - default: tilfældig │ │
│ │ • SOCKS5 via 10.99.0.{10+id}:1080 │ │
│ │ • Streamer response chunked tilbage │ │
│ └─────────────────┬──────────────────────────────┘ │
│ │ │
│ ┌──────┴──────┐ br-weircon (10.99.0.1/24)│
│ │ bridge │ │
│ └──┬───┬───┬──┘ │
│ veth ────┘ │ └──── veth │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ netns: │ │ netns: │ │ netns: │ …× 10 │
│ │ proxy0 │ │ proxy1 │ │ proxy9 │ │
│ │ veth: │ │ veth: │ │ veth: │ │
│ │10.99.0.10│ │10.99.0.11│ │10.99.0.19│ │
│ │ + wg0 │ │ + wg0 │ │ + wg0 │ │
│ │ + micro │ │ + micro │ │ + micro │ │
│ │ socks │ │ socks │ │ socks │ │
│ │ :1080 │ │ :1080 │ │ :1080 │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ ▼ ▼ ▼ │
│ Proton Proton Proton (via wg0) │
└──────────────────────────────────────────────────────┘
```
## Klient-API
Eksempel — hent en URL gennem en tilfældig tunnel:
```bash
curl -H "X-WEIRCON-RANDOM-IP: $API_KEY" \
-H "X-WEIRCON-RANDOM-IP-REDIRECT: https://api.ipify.org" \
https://random-proxy.weircon.dk/
```
Eller via query param:
```bash
curl -H "X-WEIRCON-RANDOM-IP: $API_KEY" \
"https://random-proxy.weircon.dk/?url=https://api.ipify.org"
```
Pin en specifik tunnel:
```bash
curl -H "X-WEIRCON-RANDOM-IP: $API_KEY" \
-H "X-WEIRCON-PROXY-ID: 3" \
"https://random-proxy.weircon.dk/?url=https://api.ipify.org"
```
### Headers / parametre
| Hvor | Navn | Krævet | Forklaring |
| ----------------- | ----------------------------- | ------ | ----------------------------------------------------------- |
| NPM (header) | `X-WEIRCON-RANDOM-IP` | ja | API-key. Verificeres af nginx og strippes før forward. |
| Header eller query| `X-WEIRCON-RANDOM-IP-REDIRECT`/`?url=` | ja | Mål-URL der skal hentes. Header har præcedens over query. |
| Header | `X-WEIRCON-PROXY-ID` | nej | `0`-`9`. Hvis udeladt: random. |
| Header | `X-WEIRCON-FORWARD-METHOD` | nej | HTTP-metode mod target. Default: samme som indkommende. |
| Body | _passthrough_ | nej | Body videresendes som-er hvis metoden er POST/PUT/PATCH. |
### Response
Status + headers + body fra upstream returneres direkte. Plus `X-WEIRCON-EGRESS-PROXY: <id>` så klient kan se hvilken tunnel der blev brugt.
## Komponenter
| Sti | Formål |
| ---------------------- | -------------------------------------------------------------------------- |
| `service/main.go` | Go fetch-service (statisk binary, kører i hoved-netns i LXC'en). |
| `service/Makefile` | `make build` → 6MB static binary; `make install` til /usr/local/bin. |
| `lxc/setup-host.md` | Proxmox-side: `pct create`, TUN passthrough, push-kommandoer. |
| `lxc/setup-container.sh` | Kører **inde i** LXC'en: apt-deps, microsocks, helper-scripts, units. |
| `lxc/netns-up.sh` | Bringer ét namespace op: `proxy<N>` med wg0 + veth + bridge. |
| `lxc/netns-down.sh` | River det ned igen (kaldes af systemd ExecStopPost). |
| `lxc/systemd/weircon-proxies.target` | Grouping target for alle 10 tunneler. |
| `lxc/systemd/weircon-proxy@.service` | Templated unit. `enable weircon-proxy@{0..9}.service`. |
| `lxc/systemd/weircon-fetch.service` | Selve fetch-servicen. Hardened (DynamicUser, no caps). |
| `lxc/fetch.env.example` | Default env til fetch-servicen — kopieres til `/etc/weircon-random-proxy/fetch.env`. |
| `npm/advanced.conf` | NPM "Advanced" tab snippet: header-check + strip + forward. |
| `wg-configs/` | Lokal placeholder. Selve `.conf`-filerne lever på LXC'en, **ikke** i git. |
## NPM-laget (kort)
I NginxProxyManager → din proxy host → **Advanced** tab indsættes en config der:
1. Tjekker `$http_x_weircon_random_ip` mod den forventede API-key.
2. Returnerer `401` hvis den ikke matcher.
3. Stripper headeren med `proxy_set_header X-WEIRCON-RANDOM-IP "";` før forward.
4. Forwarder til LXC'ens interne IP på port 8080.
Detaljer + færdig snippet ligger i `npm/advanced.conf`.
## Service-detaljer (Go)
- `net/http/httputil.ReverseProxy` håndterer body-streaming chunked uden buffering.
- Én `*http.Transport` pr. tunnel → connection-pool genbruges pr. Proton-egress.
- `golang.org/x/net/proxy.SOCKS5` ContextDialer på hver Transport.
- Statisk binary (`CGO_ENABLED=0`), ~6MB, ingen runtime deps i LXC'en.
- Env-vars: `WEIRCON_LISTEN`, `WEIRCON_PROXY_COUNT`, `WEIRCON_PROXY_BASE_PORT`, `WEIRCON_PROXY_HOST`, `WEIRCON_REQUEST_TIMEOUT_SEC`.
Build + smoke-test:
```sh
cd service && make build
./weircon-random-proxy # lytter på :8080, peger på 127.0.0.1:25400..25409
```
## Deploy-flow (one-shot)
På din dev-maskine:
```sh
cd random_proxy/service && make build # → ./weircon-random-proxy (6MB static)
```
På Proxmox-hosten følges `lxc/setup-host.md`:
1. `pct create` med `--unprivileged 1 --features nesting=1,keyctl=1` (Debian 12).
2. Tilføj TUN passthrough i `/etc/pve/lxc/<ctid>.conf`.
3. `pct push` binary, scripts, units, og dine Proton WG-configs.
4. `pct enter <ctid> && bash /root/setup-container.sh`.
5. `systemctl enable --now weircon-proxies.target weircon-proxy@{0..9}.service weircon-fetch.service`.
6. I NPM: opret host der peger på LXC:8080, indsæt `npm/advanced.conf` i Advanced-tabben med din API-key.
## Status
- [x] Arkitektur dokumenteret
- [x] `service/` Go fetch-service (kompilerer, vet clean, smoke-tested)
- [x] `lxc/` netns scripts + systemd units + setup-guides
- [x] `npm/advanced.conf` nginx-snippet til NPM
- [ ] Faktisk deploy på Proxmox (kræver din host)
- [ ] Migrationsnote i `nh-scrape/proxy-stack/` der peger hertil
- [ ] Opdater `nh-scrape` til at kalde den nye endpoint
## Open questions
- **API-key opbevaring**: env-var i NPM, eller en fil mountet ind? (Foreslår env-var via NPM stack.)
- **Rate limiting / concurrency**: skal vi cap'e samtidige requests pr. tunnel? Proton accepterer ikke ubegrænset.
- **Logging**: hvor meget skal logges (URL, status, tunnel-id)? Husk GDPR hvis det rammer personhenførbare URL'er.
- **Failover**: hvis valgt tunnel er nede, fall back til random anden, eller fejl 503?
+9
View File
@@ -0,0 +1,9 @@
# Lytte-adresse. 0.0.0.0 så NPM kan ramme den udefra (LAN/Tailscale).
WEIRCON_LISTEN=0.0.0.0:8080
# 10 tunneler — hver netns har sin egen IP på den interne br-weircon, port 1080.
# Hvis du øger antal tunneler i netns-up.sh / weircon-proxy@.service, så udvid her tilsvarende.
WEIRCON_PROXY_ADDRS=10.99.0.10:1080,10.99.0.11:1080,10.99.0.12:1080,10.99.0.13:1080,10.99.0.14:1080,10.99.0.15:1080,10.99.0.16:1080,10.99.0.17:1080,10.99.0.18:1080,10.99.0.19:1080
# Maks tid pr. upstream-fetch (sekunder).
WEIRCON_REQUEST_TIMEOUT_SEC=30
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
# River én tunnel-namespace ned. Bridge'n (br-weircon) bliver bevidst stående
# for at undgå race med andre tunneler der starter samtidig.
set -euo pipefail
ID="${1:?usage: $0 <id 0..N>}"
NS="proxy${ID}"
VETH_MAIN="vp${ID}a"
ip link delete "$VETH_MAIN" 2>/dev/null || true
ip netns delete "$NS" 2>/dev/null || true
rm -rf "/etc/netns/${NS}"
echo "netns $NS revet ned"
+95
View File
@@ -0,0 +1,95 @@
#!/bin/bash
# Bringer én tunnel-namespace op.
# - opretter (eller genbruger) bridge br-weircon i hoved-netns
# - opretter netns proxy<id>
# - laver veth-par mellem hoved-netns og proxy<id>, attacher hoved-siden til bridge
# - flytter et frisk wg0-interface ind i ns'en og applikerer Proton-config
# - sætter default-route via wg0 inde i ns'en
# Idempotent: river først evt. tidligere state for samme id ned.
set -euo pipefail
ID="${1:?usage: $0 <id 0..N>}"
NS="proxy${ID}"
WG="wg0" # interfacenavn inde i ns'en
VETH_MAIN="vp${ID}a"
VETH_NS="vp${ID}b"
BRIDGE="br-weircon"
BRIDGE_IP="10.99.0.1"
BRIDGE_CIDR="${BRIDGE_IP}/24"
NS_IP="10.99.0.$((10 + ID))"
NS_CIDR="${NS_IP}/24"
CONF="/etc/weircon-random-proxy/wg/proxy${ID}.conf"
RESOLV_DIR="/etc/netns/${NS}"
if [[ ! -f "$CONF" ]]; then
echo "missing wg config at $CONF" >&2
exit 1
fi
# --- 1. river eventuel tidligere state ned --------------------------------
ip link delete "$VETH_MAIN" 2>/dev/null || true
ip netns delete "$NS" 2>/dev/null || true
rm -rf "$RESOLV_DIR"
# --- 2. bridge (delt af alle tunneler) ------------------------------------
if ! ip link show "$BRIDGE" >/dev/null 2>&1; then
ip link add "$BRIDGE" type bridge
ip addr add "$BRIDGE_CIDR" dev "$BRIDGE"
ip link set "$BRIDGE" up
fi
# --- 3. per-netns resolver fra DNS-feltet i Proton-conf'en ----------------
mkdir -p "$RESOLV_DIR"
: > "${RESOLV_DIR}/resolv.conf"
DNS_LINE=$(awk -F'=' '/^[[:space:]]*DNS[[:space:]]*=/ {print $2; exit}' "$CONF" || true)
if [[ -n "${DNS_LINE:-}" ]]; then
IFS=',' read -ra dns_arr <<< "$DNS_LINE"
for d in "${dns_arr[@]}"; do
d="${d// /}"
[[ -n "$d" ]] && echo "nameserver $d" >> "${RESOLV_DIR}/resolv.conf"
done
fi
# --- 4. ns + loopback ------------------------------------------------------
ip netns add "$NS"
ip -n "$NS" link set lo up
# --- 5. veth-par, hoved-siden til bridge ----------------------------------
ip link add "$VETH_MAIN" type veth peer name "$VETH_NS"
ip link set "$VETH_MAIN" master "$BRIDGE"
ip link set "$VETH_MAIN" up
ip link set "$VETH_NS" netns "$NS"
ip -n "$NS" addr add "$NS_CIDR" dev "$VETH_NS"
ip -n "$NS" link set "$VETH_NS" up
# --- 6. WG-interface: opret + konfigurer i hoved-netns, flyt så ind -------
# Endpoint-hostname (hvis Proton-config'en bruger ét) skal resolves *inden*
# interfacet flyttes — netns'en har kun Proton DNS, som kun virker når
# tunnelen allerede er oppe. Klassisk chicken-and-egg.
ip link add "$WG" type wireguard
TMPCONF=$(mktemp)
trap 'rm -f "$TMPCONF"' EXIT
wg-quick strip "$CONF" > "$TMPCONF"
wg setconf "$WG" "$TMPCONF" # resolves via hoved-netns DNS
ip link set "$WG" netns "$NS"
# Interface-adresser fra [Interface] Address = ...
while read -r ADDR; do
[[ -z "$ADDR" ]] && continue
ip -n "$NS" addr add "$ADDR" dev "$WG"
done < <(awk -F'=' '
/^[[:space:]]*Address[[:space:]]*=/ {
gsub(/[[:space:]]/, "", $2);
n = split($2, a, ",");
for (i = 1; i <= n; i++) print a[i];
}' "$CONF")
ip -n "$NS" link set "$WG" up
# --- 7. default route via wg (egress til internettet) --------------------
ip -n "$NS" route add default dev "$WG"
echo "netns $NS oppe; socks5 vil lytte på ${NS_IP}:1080"
+87
View File
@@ -0,0 +1,87 @@
#!/bin/bash
# Køres som root inde i LXC'en. Idempotent.
set -euo pipefail
if [[ "$EUID" -ne 0 ]]; then
echo "must be run as root" >&2
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# 1. APT deps
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends \
wireguard-tools \
iproute2 \
iptables \
ca-certificates \
git \
build-essential \
procps
# 2. Build + install microsocks (lille, ingen runtime deps)
if ! command -v microsocks >/dev/null 2>&1; then
tmp=$(mktemp -d)
git clone --depth=1 https://github.com/rofl0r/microsocks.git "$tmp"
make -C "$tmp"
install -Dm755 "$tmp/microsocks" /usr/local/bin/microsocks
rm -rf "$tmp"
fi
# 3. Install fetch-service binary (forventes pushet ind af host-scriptet)
if [[ ! -x /usr/local/bin/weircon-random-proxy ]]; then
echo "ERR: /usr/local/bin/weircon-random-proxy mangler — push den fra host'en først" >&2
exit 1
fi
# 4. Helper-scripts
install -Dm755 "$SCRIPT_DIR/netns-up.sh" /usr/local/sbin/weircon-netns-up
install -Dm755 "$SCRIPT_DIR/netns-down.sh" /usr/local/sbin/weircon-netns-down
# 5. systemd units
install -Dm644 "$SCRIPT_DIR/weircon-proxies.target" /etc/systemd/system/weircon-proxies.target
install -Dm644 "$SCRIPT_DIR/weircon-proxy@.service" /etc/systemd/system/weircon-proxy@.service
install -Dm644 "$SCRIPT_DIR/weircon-fetch.service" /etc/systemd/system/weircon-fetch.service
# 6. Config-dir + default env
mkdir -p /etc/weircon-random-proxy/wg
chmod 700 /etc/weircon-random-proxy/wg
if [[ ! -f /etc/weircon-random-proxy/fetch.env ]]; then
install -m640 "$SCRIPT_DIR/fetch.env.example" /etc/weircon-random-proxy/fetch.env
fi
systemctl daemon-reload
# 7. Tjek at WG-configs er på plads
missing=0
for i in 0 1 2 3 4 5 6 7 8 9; do
if [[ ! -f /etc/weircon-random-proxy/wg/proxy${i}.conf ]]; then
echo "WARN: /etc/weircon-random-proxy/wg/proxy${i}.conf mangler"
missing=1
fi
done
cat <<EOF
Setup done.
Configs til stede: $([[ $missing -eq 0 ]] && echo "alle 10 OK" || echo "MANGLER — push dem ind før du starter")
Start stakken:
systemctl enable --now weircon-proxies.target
systemctl enable --now weircon-proxy@{0..9}.service
systemctl enable --now weircon-fetch.service
Verificér:
systemctl status 'weircon-proxy@*'
curl http://127.0.0.1:8080/health
curl -H 'X-Weircon-Random-Ip-Redirect: https://api.ipify.org' http://127.0.0.1:8080/
Egress-IP tjek (skal vise N distinkte Proton-IPs):
for i in 0 1 2 3 4 5 6 7 8 9; do
curl -sS -H "X-Weircon-Proxy-Id: \$i" -H "X-Weircon-Random-Ip-Redirect: https://api.ipify.org" http://127.0.0.1:8080/
echo " ← proxy\$i"
done
EOF
+88
View File
@@ -0,0 +1,88 @@
# Proxmox host setup (one-time)
Alt herfra køres på Proxmox-hosten som root. Når LXC'en er oppe, skip videre til `setup-container.sh`.
## 1. Opret unprivileged container
Brug Debian 12 (eller nyere). Tilpas `200` (CTID), storage og bridge til din opsætning.
```sh
pveam download local debian-12-standard_12.7-1_amd64.tar.zst # hvis ikke allerede til stede
pct create 200 local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst \
--hostname weircon-random-proxy \
--unprivileged 1 \
--features nesting=1,keyctl=1 \
--memory 1024 \
--swap 512 \
--cores 2 \
--rootfs local-lvm:8 \
--net0 name=eth0,bridge=vmbr0,ip=dhcp \
--onboot 1
```
> **Hvorfor `nesting=1`**: WireGuard inde i en netns inde i en unprivileged LXC kræver at flere user-/mount-namespaces kan stables.
> **Hvorfor ikke `nesting=0`**: så afviser kernen `ip netns add` i containeren.
## 2. TUN passthrough
Tilføj til `/etc/pve/lxc/200.conf`:
```
lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
```
Kernel-WireGuard bruger som regel en wireguard-netdev og kræver strengt taget ikke `/dev/net/tun` — men `wg-quick` og enkelte VPN-værktøjer rører ved den, og det skader ikke at have den tilgængelig.
Tjek at WireGuard-modulet er loaded på *hosten*:
```sh
modprobe wireguard
lsmod | grep wireguard
echo wireguard >> /etc/modules-load.d/wireguard.conf
```
## 3. Start container + indlæs filer
```sh
pct start 200
# Byg fetch-service binæren på din dev-maskine (Arch eller hvor):
# cd random_proxy/service && make build
# Push den + setup-scriptet ind:
pct push 200 ./service/weircon-random-proxy /usr/local/bin/weircon-random-proxy --perms 0755
pct push 200 ./lxc/setup-container.sh /root/setup-container.sh --perms 0755
pct push 200 ./lxc/netns-up.sh /root/netns-up.sh --perms 0755
pct push 200 ./lxc/netns-down.sh /root/netns-down.sh --perms 0755
pct push 200 ./lxc/systemd/weircon-proxies.target /root/weircon-proxies.target
pct push 200 ./lxc/systemd/weircon-proxy@.service /root/weircon-proxy@.service
pct push 200 ./lxc/systemd/weircon-fetch.service /root/weircon-fetch.service
pct push 200 ./lxc/fetch.env.example /root/fetch.env.example
# Proton WG-configs (én pr. tunnel)
for i in 0 1 2 3 4 5 6 7 8 9; do
pct push 200 ./wg-configs/proxy${i}.conf /etc/weircon-random-proxy/wg/proxy${i}.conf --perms 0600
done
# Hop ind og kør resten
pct enter 200
bash /root/setup-container.sh
```
## 4. Health check fra hosten
Hvis LXC'ens IP er fx 10.0.0.50:
```sh
curl http://10.0.0.50:8080/health
# {"ok":true,"proxies":10}
```
## 5. Peg NPM på containeren
I NginxProxyManager:
- Forward Hostname / IP: `10.0.0.50`
- Forward Port: `8080`
- Tab "Advanced": indsæt indholdet af `npm/advanced.conf` (husk at udskifte placeholder API-key).
+31
View File
@@ -0,0 +1,31 @@
[Unit]
Description=weircon-random-proxy fetch service
After=network-online.target weircon-proxies.target
Wants=network-online.target weircon-proxies.target
[Service]
Type=simple
EnvironmentFile=/etc/weircon-random-proxy/fetch.env
ExecStart=/usr/local/bin/weircon-random-proxy
Restart=on-failure
RestartSec=3
# Service skal kun lytte i hoved-netns og må ikke køre som root.
DynamicUser=yes
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictNamespaces=yes
RestrictRealtime=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
CapabilityBoundingSet=
AmbientCapabilities=
[Install]
WantedBy=multi-user.target
+6
View File
@@ -0,0 +1,6 @@
[Unit]
Description=All weircon-random-proxy tunnels
StopWhenUnneeded=no
[Install]
WantedBy=multi-user.target
+17
View File
@@ -0,0 +1,17 @@
[Unit]
Description=weircon-random-proxy tunnel %i (Proton WG + microsocks in netns proxy%i)
After=network-online.target
Wants=network-online.target
PartOf=weircon-proxies.target
[Service]
Type=simple
ExecStartPre=/usr/local/sbin/weircon-netns-up %i
ExecStart=/usr/sbin/ip netns exec proxy%i /usr/local/bin/microsocks -i 0.0.0.0 -p 1080
ExecStopPost=-/usr/local/sbin/weircon-netns-down %i
Restart=on-failure
RestartSec=5
TimeoutStartSec=30
[Install]
WantedBy=weircon-proxies.target
+28
View File
@@ -0,0 +1,28 @@
# Indsæt i NginxProxyManager → din proxy host → tab "Advanced".
#
# Forward Hostname/IP og Forward Port sættes som normalt via UI'et (peg på
# LXC'ens interne IP, port 8080). Denne config laver kun headerauth + stripping.
# 1. API-key check. Skift placeholderen ud med en lang random værdi (>= 32 chars).
# Eksempel-generering: openssl rand -hex 32
set $weircon_api_key "REPLACE_WITH_A_LONG_RANDOM_API_KEY";
if ($http_x_weircon_random_ip != $weircon_api_key) {
return 401;
}
# 2. Strip auth-headeren før forward — backenden skal aldrig se den.
proxy_set_header X-Weircon-Random-Ip "";
# 3. Lange timeouts: scraping-targets kan være langsomme.
proxy_connect_timeout 15s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# 4. Ingen buffering — lad bodyen streame igennem.
proxy_buffering off;
proxy_request_buffering off;
proxy_http_version 1.1;
# 5. Stort body-loft hvis du nogensinde POST'er noget tungt videre.
client_max_body_size 32m;
+19
View File
@@ -0,0 +1,19 @@
BINARY := weircon-random-proxy
PREFIX ?= /usr/local
.PHONY: build run tidy clean install
build:
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o $(BINARY) .
run:
go run .
tidy:
go mod tidy
clean:
rm -f $(BINARY)
install: build
install -Dm755 $(BINARY) $(DESTDIR)$(PREFIX)/bin/$(BINARY)
+5
View File
@@ -0,0 +1,5 @@
module weircon.dk/random-proxy
go 1.22
require golang.org/x/net v0.30.0
+2
View File
@@ -0,0 +1,2 @@
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
+261
View File
@@ -0,0 +1,261 @@
// weircon-random-proxy — fetch-service der videresender HTTP-requests gennem
// en af N lokale SOCKS5-tunneler (én pr. Proton WireGuard peer).
//
// Bag NginxProxyManager. NPM verificerer X-WEIRCON-RANDOM-IP og stripper den
// før forward, så denne service lytter kun internt og forudsætter at auth er
// håndteret upstream.
package main
import (
"context"
"errors"
"fmt"
"log"
"math/rand/v2"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strconv"
"strings"
"time"
"golang.org/x/net/proxy"
)
type Config struct {
Listen string
ProxyAddrs []string
Timeout time.Duration
}
func loadConfig() (Config, error) {
cfg := Config{
Listen: envStr("WEIRCON_LISTEN", ":8080"),
Timeout: time.Duration(envInt("WEIRCON_REQUEST_TIMEOUT_SEC", 30)) * time.Second,
}
if raw := envStr("WEIRCON_PROXY_ADDRS", ""); raw != "" {
for _, a := range strings.Split(raw, ",") {
a = strings.TrimSpace(a)
if a == "" {
continue
}
cfg.ProxyAddrs = append(cfg.ProxyAddrs, a)
}
} else {
host := envStr("WEIRCON_PROXY_HOST", "127.0.0.1")
base := envInt("WEIRCON_PROXY_BASE_PORT", 25400)
count := envInt("WEIRCON_PROXY_COUNT", 10)
for i := 0; i < count; i++ {
cfg.ProxyAddrs = append(cfg.ProxyAddrs, fmt.Sprintf("%s:%d", host, base+i))
}
}
if len(cfg.ProxyAddrs) == 0 {
return cfg, errors.New("no proxy addrs configured")
}
for i, a := range cfg.ProxyAddrs {
if _, _, err := net.SplitHostPort(a); err != nil {
return cfg, fmt.Errorf("invalid proxy addr [%d] %q: %w", i, a, err)
}
}
return cfg, nil
}
func envStr(k, def string) string {
if v, ok := os.LookupEnv(k); ok && v != "" {
return v
}
return def
}
func envInt(k string, def int) int {
if v, ok := os.LookupEnv(k); ok && v != "" {
if n, err := strconv.Atoi(v); err == nil {
return n
}
}
return def
}
// buildTransports creates one *http.Transport per backend SOCKS5 tunnel so
// each Proton egress maintains its own connection pool.
func buildTransports(cfg Config) ([]*http.Transport, error) {
pool := make([]*http.Transport, len(cfg.ProxyAddrs))
for i, addr := range cfg.ProxyAddrs {
base := &net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}
d, err := proxy.SOCKS5("tcp", addr, nil, base)
if err != nil {
return nil, fmt.Errorf("socks5 dialer proxy%d: %w", i, err)
}
ctxDialer, ok := d.(proxy.ContextDialer)
if !ok {
return nil, fmt.Errorf("socks5 dialer proxy%d lacks ContextDialer", i)
}
pool[i] = &http.Transport{
DialContext: ctxDialer.DialContext,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 15 * time.Second,
ResponseHeaderTimeout: cfg.Timeout,
ExpectContinueTimeout: 1 * time.Second,
}
}
return pool, nil
}
type ctxKey struct{}
type reqCtx struct {
proxyID int
target *url.URL
method string
}
// poolTransport routes each RoundTrip to the per-tunnel transport carried in
// the request context.
type poolTransport struct{ pool []*http.Transport }
func (p *poolTransport) RoundTrip(req *http.Request) (*http.Response, error) {
rc, ok := req.Context().Value(ctxKey{}).(reqCtx)
if !ok {
return nil, errors.New("internal: missing reqCtx")
}
return p.pool[rc.proxyID].RoundTrip(req)
}
var weirconHeaders = []string{
"X-Weircon-Random-Ip",
"X-Weircon-Random-Ip-Redirect",
"X-Weircon-Proxy-Id",
"X-Weircon-Forward-Method",
}
func stripWeircon(h http.Header) {
for _, k := range weirconHeaders {
h.Del(k)
}
}
func pickProxyID(req *http.Request, n int) (int, error) {
raw := req.Header.Get("X-Weircon-Proxy-Id")
if raw == "" {
return rand.IntN(n), nil
}
id, err := strconv.Atoi(raw)
if err != nil {
return 0, fmt.Errorf("X-WEIRCON-PROXY-ID not an int: %w", err)
}
if id < 0 || id >= n {
return 0, fmt.Errorf("X-WEIRCON-PROXY-ID out of range (0-%d)", n-1)
}
return id, nil
}
func parseTarget(req *http.Request) (*url.URL, error) {
raw := req.Header.Get("X-Weircon-Random-Ip-Redirect")
if raw == "" {
raw = req.URL.Query().Get("url")
}
if raw == "" {
return nil, errors.New("missing target — set X-WEIRCON-RANDOM-IP-REDIRECT or ?url=")
}
u, err := url.Parse(raw)
if err != nil {
return nil, fmt.Errorf("invalid target URL: %w", err)
}
if u.Scheme != "http" && u.Scheme != "https" {
return nil, errors.New("target URL must be http(s)")
}
if u.Host == "" {
return nil, errors.New("target URL missing host")
}
return u, nil
}
func resolveMethod(req *http.Request) string {
if m := req.Header.Get("X-Weircon-Forward-Method"); m != "" {
return strings.ToUpper(m)
}
return req.Method
}
func newHandler(cfg Config, pool []*http.Transport) http.Handler {
rp := &httputil.ReverseProxy{
Rewrite: func(pr *httputil.ProxyRequest) {
rc, ok := pr.In.Context().Value(ctxKey{}).(reqCtx)
if !ok {
return
}
pr.Out.URL = rc.target
pr.Out.Host = rc.target.Host
pr.Out.Method = rc.method
pr.Out.RequestURI = ""
stripWeircon(pr.Out.Header)
},
Transport: &poolTransport{pool: pool},
ModifyResponse: func(resp *http.Response) error {
if rc, ok := resp.Request.Context().Value(ctxKey{}).(reqCtx); ok {
resp.Header.Set("X-Weircon-Egress-Proxy", strconv.Itoa(rc.proxyID))
}
return nil
},
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
id := -1
if rc, ok := r.Context().Value(ctxKey{}).(reqCtx); ok {
id = rc.proxyID
}
log.Printf("upstream error via proxy%d: %v", id, err)
http.Error(w, fmt.Sprintf("upstream via proxy%d failed: %v", id, err), http.StatusBadGateway)
},
}
count := len(pool)
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"ok":true,"proxies":%d}`, count)
})
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
target, err := parseTarget(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
id, err := pickProxyID(r, count)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
ctx := context.WithValue(r.Context(), ctxKey{}, reqCtx{
proxyID: id,
target: target,
method: resolveMethod(r),
})
rp.ServeHTTP(w, r.WithContext(ctx))
})
return mux
}
func main() {
cfg, err := loadConfig()
if err != nil {
log.Fatalf("config: %v", err)
}
pool, err := buildTransports(cfg)
if err != nil {
log.Fatalf("transports: %v", err)
}
srv := &http.Server{
Addr: cfg.Listen,
Handler: newHandler(cfg, pool),
ReadHeaderTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
log.Printf("weircon-random-proxy listening on %s (proxies=%d: %s)",
cfg.Listen, len(cfg.ProxyAddrs), strings.Join(cfg.ProxyAddrs, ","))
if err := srv.ListenAndServe(); err != nil {
log.Fatalf("server: %v", err)
}
}
View File