- DOCS/Operations: current-state SUMMARY of the ENTRY deployment (access, install, egress via Hysteria2, security findings and fixes, risk assessment, commits/backups, open items), the Hysteria chain manifest with an OpenVPN Monitor section, reboot test results, and the plan and rollout records for settings validation and privilege separation. - Link them from README and DOCS/General/Index.md. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
14 KiB
Hysteria 2 entry→exit chain (manifest)
Client ─hy2/UDP443→ ENTRY (Alpine, OpenRC) ─hy2/UDP443→ EXIT (Debian, systemd) → Internet (IPv4)
Secrets live only in: client-uris.txt, singbox-*.json, /etc/hysteria/* on hosts (chmod 600). Never commit/print them.
Inventory (this deployment)
| Role | Host | OS | Access |
|---|---|---|---|
| ENTRY (Timeweb) | 213.226.125.13 | Alpine 3.23 | ssh -i private.key root@ (key must be chmod 600) |
| EXIT (Gcore Hel) | 85.234.86.140 | Debian 13 | ssh -i hpriv.key debian@ (sudo NOPASSWD) |
Prerequisites (verify first, they were the real blockers)
- UDP 443 open both ways (Security Group on EXIT; ENTRY provider). Test:
tcpdump -ni <if> udp port 443on the receiver whileecho x | nc -u -w1 <ip> 443from the sender. ICMP/TCP passing proves nothing about UDP. - Use official binary
apernet/hysteria(HyNetworks/hysteria is a fork). Verifysha256sumagainsthashes.txtof the release:https://github.com/apernet/hysteria/releases/latest/download/{hysteria-linux-amd64,hashes.txt} - Hysteria server
socks5/httpoutbounds are TCP-only → for UDP use TUN + policy routing (below), not a SOCKS5 chain.
EXIT (Debian) — server
/usr/local/bin/hysteria,/etc/hysteria/{server.crt,server.key(600),config.yaml}- Self-signed cert:
openssl req -x509 -nodes -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -days 3650 -subj /CN=<name> -keyout server.key -out server.crt config.yaml:listen: :443,tls{cert,key},auth{type: password, password: <rand>},masquerade{type: proxy, proxy{url: https://news.ycombinator.com/, rewriteHost: true}}- systemd unit
hysteria-server(ExecStart=hysteria server -c ...,Restart=always,AmbientCapabilities=CAP_NET_BIND_SERVICE) /etc/sysctl.d/99-quic.conf:net.core.rmem_max=7500000,net.core.wmem_max=7500000- No IPv6 uplink here → IPv4 only.
ENTRY (Alpine) — tunnel client + client-facing server
Packages: iproute2 libcap-utils openssl; user hyedge (adduser -S -D -H -s /sbin/nologin); /dev/net/tun must exist and module tun must be in /etc/modules (loaded module, not builtin; without it the tunnel does not come up after reboot).
/etc/hysteria/client.yaml(600):server: <EXIT>:443,auth: <pw>,tls{sni, insecure: true, pinSHA256: <EXIT cert fp>},lazy: true,bandwidth{up: 180 mbps, down: 180 mbps}(Brutal CC; measured path ≈191-195 Mbps viahysteria speedtest, single-flow without it was ~44 Mbps),socks5{127.0.0.1:1080},http{127.0.0.1:8080},tun{name: hytun, mtu: 1400, timeout: 5m, address{ipv4: 100.100.100.101/30}}(noroute:→ no auto-routes)./etc/hysteria/edge.yaml(600, owner hyedge):listen: :443, own self-signededge.crt/edge.key,auth{type: userpass, userpass{<dev>: <pw>}},resolver{type: udp, udp{addr: 1.1.1.1:53}},outbounds[direct{mode: "4"}],trafficStats{listen: 127.0.0.1:9999, secret},acl.inline:reject10/8,172.16/12,192.168/16,127/8,169.254/16,100.64/10 thendirect(all).- Do not add
reject(::/0)— it matches any domain having AAAA and rejects dual-stack sites. - DoH resolver timed out via tun; UDP resolver works.
- Do not add
- Binary copy for low port:
cp hysteria hysteria-edge; setcap cap_net_bind_service=+ep /usr/local/bin/hysteria-edge. - OpenRC (
supervisor=supervise-daemon,respawn_delay=3,respawn_max=0):hysteria(client, root) ←need nethysteria-route←need hysteria; waits forhytun, then:ip route replace blackhole default metric 1000 table 100 # anti-leak fallback ip route replace default dev hytun metric 10 table 100 ip rule add priority 100 ipproto udp sport 443 lookup main # edge replies bypass tun ip rule add priority 101 uidrange <uid hyedge> lookup 100hysteria-edge(command_user=hyedge) ←need net hysteria-routerc-update add hysteria/hysteria-route/hysteria-edge default
- sysctl (critical):
net.ipv4.conf.all.rp_filter=2anddefault=2(/etc/sysctl.d/99-hytun.conf). With strict1the TUN SYN-ACKs are dropped → TCP via tun silently hangs. - Only uid
hyedgetraffic and, since 2026-09-30, the OpenVPN client subnet (172.20.1.0/24, rule 102, see below) enter the tunnel; SSH/root traffic untouched.
OpenVPN Monitor & Profiler on ENTRY (added 2026-09-30)
ENTRY also runs the web suite for OpenVPN (native OpenRC services, no containers). OpenVPN clients exit through the same hytun tunnel to EXIT.
| Item | Value |
|---|---|
| Panel | https://213.226.125.13:8088/ (self-signed TLS, login tstark, 2FA; password is set by the owner, not stored here) |
| OpenVPN | udp/1194, clients 172.20.1.0/24, full tunnel |
| Services | ovpmon-api, ovpmon-gatherer, ovpmon-profiler (user ovpmon), nginx, openvpn, ovpn-hytun, fail2ban |
| Egress | ip rule 102 from 172.20.1.0/24 → table 100 (hytun, blackhole fallback) + MASQUERADE on hytun + FORWARD DROP to any other interface (no leak) |
| Privileges | APIs run unprivileged; a root helper (ovpmon-helper via doas, 6 fixed commands) validates/installs server.conf, publishes the CRL and controls openvpn |
| Hardening | SSH key-only (00-hardening.conf), fail2ban (sshd, sshd-ddos, ovpmon-login), input validation, CORS restricted |
| Docs | SUMMARY-ovpn-monitor.md (state, results, security assessment, rollback), IMPLEMENTATION-01/02-*.md, PLAN-validation-and-privilege-drop.md; application repo DOCS/ |
Check: rc-status | grep -E "ovpmon|openvpn|ovpn-hytun|nginx|fail2ban", ip rule | grep 102, a VPN client on https://ifconfig.me shows the EXIT IP (85.234.86.140).
Clients
- URI:
hy2://<user>:<pw>@<ENTRY>:443/?sni=<edge sni>&insecure=1&pinSHA256=<hex, no colons>#name(strip#namewhen scripting pin extraction). - Hiddify/NekoBox/Streisand/Shadowrocket: paste URI. sing-box (SFA/SFI, ≥1.12): JSON profile with
hysteria2outbound,password: "<user>:<pw>", embeddedtls.certificatePEM (edge.crt), TUN inbound, DoH via outbound,hijack-dns, private + ENTRY IP → direct.
Recommended client apps
Protocol: Hysteria 2 (QUIC/UDP 443). Server cert is self-signed, so the app must support pinSHA256/custom certificate — all apps below do via the URI/QR or sing-box JSON.
| Platform | App | Import | Notes |
|---|---|---|---|
| Android | Hiddify (primary) | hy2:// URI or QR | sing-box core, pin support, simplest |
| Android | NekoBox for Android | hy2:// URI or QR | alternative, more knobs |
| Android | SFA (sing-box) | JSON profile only | official sing-box client; use profiles/singbox/*.json |
| iOS | Streisand (primary) | hy2:// URI or QR | free |
| iOS | Shadowrocket | hy2:// URI or QR | paid, stable |
| iOS | SFI (sing-box) / Hiddify | JSON profile / URI | sing-box >= 1.12 required for the JSON |
| Desktop | Hiddify, sing-box CLI, hysteria client |
URI / JSON / YAML | sing-box needs root/admin for TUN |
Required client settings (otherwise many sites fail or hang):
- IPv6 off (Hiddify: IPv6 mode = Disable / IPv4 only). EXIT has no IPv6; apps sending IPv6 literals get
no IPv4 address available. - Remote DNS via the tunnel (DoH
https://1.1.1.1/dns-query); do not leak DNS outside. - QUIC blocking is not needed (UDP is carried end to end).
- Verify:
ifconfig.meshows the EXIT IP (85.234.86.140);test-ipv6.comshows no IPv6.
App availability/names vary by store and region and change over time; this list comes from the project's client testing (Hiddify confirmed working on a phone) and general knowledge, not a current store check. One profile per person.
Profiles (20 client users)
profiles/ (dir 700, files 600; contains secrets): hy2/clientNN.txt (hy2:// URI), singbox/clientNN.json (sing-box >=1.12), qr/<user>.png (QR of the hy2:// URI, 22 files incl. laptop/mobile; made with qrencode -t PNG -s 8 -m 2 -l M; decoding verified), ALL-hy2.txt, INDEX.md (no passwords). Mobile apps: scan QR -> imports hy2:// profile. Users client01..client20 live in edge.yaml auth.userpass (backup edge.yaml.bak4; laptop, mobile kept). Wrong password -> client error authentication error, HTTP status code: 404.
Add/revoke a user: edit auth.userpass on ENTRY, rc-service hysteria-edge restart (all sessions reconnect within seconds), then regenerate/delete the profile files. Verified login for client01/10/20 via temp client (egress = EXIT IP).
Verification checklist
su -s /bin/sh hyedge -c 'curl -4 -s https://ifconfig.me' # = EXIT ip (TCP via tun)
su -s /bin/sh hyedge -c 'nslookup example.com 1.1.1.1' # UDP via tun
ip route del default dev hytun table 100; <same curl must FAIL>; ip route replace default dev hytun metric 10 table 100
hysteria client (temp, socks 127.0.0.1:1081) -> curl -x socks5h://... https://ifconfig.me # = EXIT ip; http://10.0.0.1 rejected
Logs: ENTRY /var/log/hysteria.log, /var/log/hysteria-edge.log; EXIT journalctl -u hysteria-server.
Tuning / diagnostics learned
- Path MTU ENTRY↔EXIT = 1500 (DF ping + tracepath); hytun mtu 1400 is safe; MTU is not the cause of site failures here.
- Capacity test: on EXIT temporarily add
speedTest: trueto config.yaml, restart, runhysteria speedtest -c /etc/hysteria/client.yamlon ENTRY, then remove it. su hyedge -c curl <domain>is ~4s slower (system resolver = provider DNS, unreachable via EXIT). Not a client issue: hysteria-edge uses its ownresolver(1.1.1.1). Use--resolveor-x socks5hwhen benchmarking.- Clients that connect to IPv6 literals get
no IPv4 address available(EXIT has no IPv6). Fix on the client: IPv6 off / IPv4-only DNS strategy (Hiddify: IPv6 mode = disable). Server cannot fix it without EXIT IPv6. UdpRcvbufErrorsrises at ~190 Mbps (quic-go requests 7 MB socket buffers; rmem_max already 7.5 MB) — cosmetic.
Hysteria diagnostics (manual)
Version: v2.12.3. Replace IPs/paths if the layout differs. On ENTRY run as root.
Service state
# ENTRY (OpenRC)
for s in hysteria hysteria-route hysteria-edge; do rc-service $s status; done
ss -lunp | grep -E ':443\b' # edge listener (hysteria-edge)
ss -unp | grep hysteria # client UDP socket to EXIT
tail -f /var/log/hysteria.log /var/log/hysteria-edge.log
# EXIT (systemd)
systemctl status hysteria-server --no-pager; ss -lunp | grep :443
journalctl -u hysteria-server -f # "client connected" / "TCP error"
Tunnel (ENTRY -> EXIT)
hysteria ping -c /etc/hysteria/client.yaml 1.1.1.1:443 # RTT of a connect via the tunnel
hysteria speedtest -c /etc/hysteria/client.yaml # needs `speedTest: true` on EXIT (remove afterwards)
ip -br a show hytun; ip rule | grep -E '^10[01]'; ip route show table 100
su -s /bin/sh hyedge -c 'curl -4 -s https://ifconfig.me' # must print the EXIT IP
tcpdump -ni hytun -c 20 # traffic entering the tun
tcpdump -ni eth0 'udp port 443 and host 85.234.86.140' # QUIC to EXIT (on EXIT: enp3s0, host 213.226.125.13)
- Client log
connected to server ... count: N– N grows on each reconnect.TUN UDP error ... EOFright after an EXIT restart is normal. rc-service hysteria status=crashedandconnect error: timeout-> UDP 443 path/Security Group problem (see prerequisites).
Clients (Traffic Stats API on edge, enabled, 127.0.0.1 only)
Config block in edge.yaml: trafficStats: {listen: 127.0.0.1:9999, secret: <in /etc/hysteria/.stats-secret, 600>}.
S=$(cat /etc/hysteria/.stats-secret); A="Authorization: $S"; U=http://127.0.0.1:9999
curl -s -H "$A" $U/online # {"mobile":1} devices per user
curl -s -H "$A" $U/traffic # {"mobile":{"tx":..,"rx":..}} add ?clear=1 to reset counters
curl -s -H "$A" -H 'Accept: text/plain' $U/dump/streams # live streams: user, bytes, lifetime, req-addr
curl -s -H "$A" -X POST -d '["mobile"]' $U/kick # drop a user's sessions (they reconnect if creds still valid)
Without the header the API returns 401. To block a user permanently remove it from auth.userpass and restart hysteria-edge.
Useful subcommands
hysteria share -c /etc/hysteria/client.yaml # hy2:// URI for a client config (prints the password!)
hysteria check-update # compare with the latest release
hysteria version
Error -> meaning
| Log text | Meaning |
|---|---|
no IPv4 address available (edge) |
client asked for an IPv6 literal; EXIT has no IPv6 -> disable IPv6 on the client |
rejected (edge) |
ACL hit (private ranges) |
resolve error ... deadline exceeded (edge) |
edge resolver unreachable through the tunnel |
no certificate matches the pinned hash |
wrong pinSHA256 (strip #name from the URI fragment) |
authentication failed / 404 on auth |
wrong user:password |
curl via tun hangs, tcpdump -i hytun shows SYN-ACK from dst on hytun |
strict rp_filter -> set =2 |
Known issues / notes
- Backups on ENTRY:
/etc/hysteria/*.bak*. Remote shell: zsh does not word-split variables → use functions for ssh wrappers. - ENTRY reboot tested (2026-09-30): all three services, hytun, rules, sysctl and tunnel came up by themselves; mobile client reconnected. EXIT reboot tested too: hysteria-server and sysctl came up, ENTRY client reconnected automatically (count 2).
- strongSwan/IKE to Gcore (62.112.222.168) is a separate, unresolved task (needs UDP 500/4500 reachability);
charonstarted manually, not enabled at boot. - SSH on ENTRY is key-only since 2026-09-30 (a cloud-init drop-in used to re-enable passwords; hardening lives in
/etc/ssh/sshd_config.d/00-hardening.conf). A full ENTRY reboot test after the OpenVPN Monitor changes passed (2026-09-30, seeREBOOT-TEST.md).