A direct, DNS, loopback or blackhole outbound is not a proxy, so the probe
must reject it instead of measuring the panel host's own reachability. The
gate compared the protocol id exactly while the core lowercases it in
LoadWithID before resolving the handler, so "Freedom" and "DNS" were not
recognised: the HTTP probe ran through the direct outbound and returned
Success=true with a full egress block, and the row's Test button stayed
enabled because isUntestable compared exactly as well. The operator reads the
panel host's own country and delay as a working tunnel.
The batch gate now folds the id once before its switch, and isUntestable goes
through the shared isOutboundProtocol helper.
* fix(outbound): read the probe protocol id and transport name like the core
The probe lane gate and the endpoint extractor behind it compared both
strings exactly, so a template the core is running was probed as something
else. With mode=tcp an outbound spelled "WireGuard" stayed in the dial-only
TCP lane, where extractOutboundEndpoints matched no case and the caller got
"No testable endpoint" for an outbound that is passing traffic.
The core lowercases a protocol id (infra/conf/loader.go) and a transport
name (TransportProtocol.Build) before it resolves either, and resolves both
"kcp" and "mkcp" to mKCP, so both readers now normalise the same way.
The panel no longer reaches the lane gate itself — the browser now sends
http for these outbounds — but the endpoint documents "tcp" for fast
dial-only probes with UDP-transport outbounds still probed over HTTP, and
that promise has to hold for direct API callers too.
* fix(outbound): read the batch probe protocol id like the core
Review of #6526 found that folding "WireGuard"/"AmneziaWG" into the UDP lane
newly routed those spellings onto two readers in buildBatchTestConfig that
still compared the id exactly. A case-variant WireGuard outbound therefore
reached the temp probe instance without noKernelTun -- which on Linux creates
a kernel TUN device alongside the live panel's own -- and a case-variant
AmneziaWG entry was appended raw, rejecting the whole temp config and
degrading the batch to serial per-item retries.
Both readers now fold the id the way the core does (infra/conf/loader.go
lowercases it before the protocol is resolved).
* feat(amneziawg): add AmneziaWG as an outbound protocol
- AmneziaWG outbound protocol end-to-end: config schema, socks bridge, netstack, panel UI
- Route amneziawg outbounds to HTTP probe in TCP mode (backend + frontend classifiers) with pinning test
- Add 2-minute idle read deadline to pumpUDPEgress to reap idle egress sessions
- Require SOCKS5 username/password auth on the egress server (reject NO-AUTH with 0xFF) with test
- Bound the egress TCP tunnel dial with portForwardDialTimeout (10s), matching portfwd.go
- Resolve UDP domain targets off the association's reader loop via deliverUDPDatagram; race-safe getOrDial starts the reply pump at session creation; client passed by value into resolver goroutines (pinned by TestEgressUDPDatagramDomainInterleavedClients)
- Reconcile early-returns on an empty desired set and closes the egress listener; EgressBasePort (64900) is reserved against local inbound port conflicts like the internal API port, with pinning tests for both the port reservation (TestCheckPortConflict_EgressPortBlockedLocal) and the Reconcile empty-desired Close/Listen lifecycle (TestOutboundManagerReconcileEmptyDesiredClosesEgress)
- Eliminate acceptLoop shutdown race by validating listener != nil and registering to tracked under s.mu before wg.Add; bound pre-auth handshake with deadline (pinned by TestEgressServerCloseDuringConcurrentAccepts)
- Support AAAA and dual-stack domain resolution in tunnel DNS resolver with v6 default fallback (DefaultTunnelDNSServerV6); add DNS field to frontend protocol form; avoid unneeded cache flushes on unchanged SetStack ticks
* fix(amneziawg): resolve IPv6-only DNS default fallback and validate required keys
- Default to IPv6 tunnel DNS on IPv6-only outbounds with blank dns
- Require non-empty secretKey and peer publicKey in ValidateAmneziaWGOutbound
- Add end-to-end IPv6 tunnel domain resolution test and test empty key rejection
- Trim comment blocks exceeding 2 lines across modified files
- Fix Storybook test execution on environments with POSIX locale
Co-Authored-By: Claude Code <noreply@anthropic.com>
---------
Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
* Add outbound egress metadata
Show egress IP and country information for outbound HTTP tests. The probe reuses the temporary SOCKS route from the existing HTTP test and fetches Cloudflare trace metadata after the reachability check succeeds.
The outbound list now adds separate Egress and Country columns, hides egress IPs until the user reveals them, and marks Cloudflare WARP results with an orange cloud pill. Mobile cards keep the same data compact by placing the country and IPv4/IPv6 values on separate lines.
Validation: npm run typecheck; npm run lint; npm run build; go test ./internal/web/service/outbound
* Use context-aware DNS lookup for egress trace
* Address outbound egress review feedback
Restore the Real Delay selector and TCP default so the egress metadata change does not remove an existing test mode.
Keep HTTP probe tests hermetic by stubbing egress trace lookups, run IPv4 and IPv6 trace fetches concurrently with a shorter diagnostic timeout, scope mobile IP reveal state per row, support keyboard activation for reveal toggles, and treat WARP+ trace values as WARP-like.
The HTTP probe reports the warm per-request round-trip, which reads
lower than the delay figure client apps show for the same server. Add a
third "real" test mode that reuses the temp-instance HTTP probe but
reports the cold request's full elapsed time - tunnel establishment
included - and skips the warm request. UDP-transport outbounds forced
out of the TCP lane still report "http"; in real mode they report
"real". The mode joins the TCP/HTTP toggle on the outbounds tab, with
the label translated in all 13 locales.
Since the batched prober replaced the single tester, the reported delay
came from one cold request with keep-alives disabled, so it stacked the
SOCKS handshake, proxy dial, proxy TLS, target TCP and target TLS on top
of the round-trip. Users upgrading from v2.9.4 - whose tester warmed the
connection first and timed a second request - saw several times the real
connection time.
The cold request still proves reachability and supplies the HTTP status
plus the connect/TLS/TTFB breakdown; the delay is now re-measured on a
second request over the kept-alive connection, falling back to the cold
total when the warm request fails. Bodies are drained (bounded) so the
connection returns to the pool, and the batch test asserts both requests
of a probe share one connection.
Add .golangci.yml (v2): the standard linters plus bodyclose, errorlint, noctx, misspell, rowserrcheck, sqlclosecheck, unconvert, usestdlibvars, with gofumpt + goimports formatters. Enable the std-error-handling exclusion preset for idiomatic Close/Remove/Setenv ignores; scope-exclude SA1019 (parser.ParseDir in tools/openapigen) and ST1005 (intentional capitalized user-facing error copy that tests assert verbatim). No inline nolint directives were introduced.
Resolve all 217 findings behavior-preserving: gofumpt/goimports formatting, explicit blank assignment on intentionally ignored errors, errors.Is/errors.As and %w wrapping, context-aware stdlib calls (CommandContext/QueryContext/NewRequestWithContext/Dialer), staticcheck simplifications, removed redundant conversions, http.StatusOK and http.MethodGet, inlined the go:fix intPtr helper, and deferred sql rows Close. Add a golangci CI job mirroring the existing Go jobs.
Test All only iterated the editable template outbounds, so subscription
outbounds (the read-only "from subscriptions" table) were never probed in
bulk. They are now queued too, keyed by tag in subscriptionTestStates so
their rows light up live; the template and subscription HTTP lanes run
serially to respect the backend's single-batch lock (TCP runs alongside).
Also stop testing freedom ("direct") and dns outbounds: they aren't
proxies, so an HTTP probe through them only measures the host's own
reachability, not a tunnel. They are now untestable in every mode -- the
per-row button is disabled and Test All skips them -- with a matching
backend guard so a direct API caller can't HTTP-test them either.
Replace the per-outbound burstObservatory polling (one temp xray spawn +
up to 15s of /debug/vars polling per outbound, serialised) with one
shared temp xray instance per batch: every tested outbound gets its own
loopback SOCKS inbound plus an inboundTag->outboundTag routing rule, and
the panel times a real HTTP request through each one in parallel. The
probe returns as soon as the response lands and records the HTTP status
plus an httptrace breakdown (proxy connect / TLS via outbound / first
byte) shown in the result popover.
New POST /panel/api/xray/testOutbounds endpoint (array in, results in
input order, max 50); the legacy /testOutbound endpoint now delegates to
the same engine. Test All chunks HTTP probes 16 per request, and a batch
whose shared process never comes up (one structurally-broken outbound
poisons the config) retries each item in an isolated instance so the
broken outbound reports xray's real error while the rest still test.