feat(gooserelay): add GooseRelay outbound under protocol/hiddify/gooserelay#22
Open
hiddifyafk wants to merge 10 commits into
Open
feat(gooserelay): add GooseRelay outbound under protocol/hiddify/gooserelay#22hiddifyafk wants to merge 10 commits into
hiddifyafk wants to merge 10 commits into
Conversation
Wraps the GooseRelayVPN carrier (hiddify/GooseRelayVPN fork) as a native sing-box outbound. Domain-fronted HTTPS to a Google Apps Script endpoint relays AES-256-GCM-encrypted frames to a VPS, mirroring how dnstt is embedded as a Hiddify-only outbound. The carrier already round-robins across script_keys and blacklists failing endpoints internally, so the outbound only owns lifecycle: PostStart launches client.Run(ctx), Diagnose() gates IsReady, Close sends RST frames via client.Shutdown. TCP only at the carrier level; UDP works via uot.Client when udp_over_tcp.enabled is set on the outbound. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
gofmt aligns the // line-comment column to the longest entry in a const group. TypeGooseRelay is the longest //H entry, so this shifts the //H column for the prior lines. No semantic change — pure whitespace. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eTimeout The constant gates the carrier.Diagnose() probe specifically, not a generic handshake. The new name makes its purpose obvious at the call site without grepping the function it controls. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hex.DecodeString runs allocator and parser even on obviously-wrong input. Cheap to short-circuit with a length check, and the separated paths give the user a more specific error: "wrong length" vs "not valid hex". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A deployment ID with /, ?, or # would inject path/query/fragment into the constructed Apps Script URL and route the request somewhere other than the user's deployment. Apps Script wouldn't honor it usefully, but defensively reject upfront with a message that points the user at the likely cause (they pasted a full URL instead of the deployment ID). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches the dnstt sibling's style: ErrorContext/InfoContext propagate the relevant context (runCtx for the carrier run goroutine, probeCtx for the diagnose probe, h.ctx for the lifecycle-level ready message) so log lines carry tracing/cancel signals through the logger pipeline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the probe used h.ctx as parent, so a Close() call mid-probe left the diagnose goroutine running until its own 10s timeout — and worse, it could flip h.started to 1 after Close had already torn down the carrier. Threading runCtx through means cancelling runCancel (which Close already does) propagates to the probe immediately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pass Previously diagnose only checked endpoints[0], so a misconfigured first key kept the outbound in failed state even when subsequent keys would have worked at runtime. Now each key gets its own throwaway *carrier.Client (no Run goroutines, just one Diagnose HTTP round-trip) and the outbound flips ready on first success — matching dnstt's "any healthy resolver wins" semantics. When one probe succeeds we cancel probeCtx, which aborts the remaining HTTP probes mid-flight. If all probes fail, we surface every per-key error as a Warn before flipping to failed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers the constructor's reject paths (missing/invalid script_keys and tunnel_key, URL separators in keys) plus the happy path that defaults Fronting and constructs a real *carrier.Client. No network I/O — Run() is never called, so the carrier just sits idle in memory until the test ends. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upstream fork (hiddify/GooseRelayVPN) reverted the internal/->pkg/ rename and now exposes a thin SDK at .../goose. Swap our imports to that single package, which: - replaces *carrier.Client with *goose.Client (a thin wrapper) - replaces carrier.Config / carrier.FrontingConfig with type aliases exported from goose - collapses NewSession + socks.NewVirtualConn into client.Dial(target) Net effect on this side: 14 lines changed in outbound.go, no behavior change. Net effect on the fork: upstream Kianmhz/GooseRelayVPN merges land cleanly forever — only the goose package is fork-specific. Pseudo-version pins to merge commit 0e68c2a3ae4c on hiddify main. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a new
gooserelayoutbound that wraps the GooseRelayVPN carrier — domain-fronted HTTPS to a Google Apps Script endpoint, AES-256-GCM-encrypted frames forwarded to a VPS — as a native Hiddify-only sing-box outbound. Lives atprotocol/hiddify/gooserelay/next todnstt, registered through the//Hblock inconstant/proxy.goandinclude/registry.go. Zero touch to vanillaprotocol/paths so upstream merges stay clean.What's in the JSON config
{ "type": "gooserelay", "tag": "goose-out", "script_keys": ["AKfycbx<deployment-id>"], "tunnel_key": "<64-char hex AES-256 key>", "google_host": "216.239.38.120:443", "sni": ["www.google.com"], "udp_over_tcp": {"enabled": false}, "handshake_timeout": "10s" }script_keysare deployment IDs (not full URLs); the outbound builds thehttps://script.google.com/macros/s/.../execURLs internally.Upstream fork dependency
Depends on a Hiddify fork of
Kianmhz/GooseRelayVPNbecause the carrier's reusable types live ininternal/*upstream. The fork ships a single thin wrapper package (goose/sdk.go) that re-exportsConfig,FrontingConfig, and aClientwith the four methods this outbound actually needs (Run,Diagnose,Shutdown,Dial). The fork's tree is otherwise byte-identical to upstream so future upstream merges land conflict-free.go.modchanges:require github.com/kianmhz/GooseRelayVPN v0.0.0-20260429125124-0e68c2a3ae4creplace github.com/kianmhz/GooseRelayVPN => github.com/hiddify/GooseRelayVPN v0.0.0-20260429125124-0e68c2a3ae4cPseudo-version pins to merge commit
0e68c2a3ae4conhiddify/GooseRelayVPNmain(PR #2 there: switch to SDK wrapper).Architecture notes
*goose.Clientper outbound. The carrier handles per-endpoint round-robin and exponential-backoff blacklisting internally, so we don't need atester.goanalog of the dnstt outbound. Significantly less code than dnstt for equivalent reliability.script_keyis probed concurrently via a throwaway one-endpoint*goose.Client.Diagnose(); the outbound flips ready on the first success and cancels remaining probes. Mirrors dnstt's "any healthy resolver wins" semantics.PostStartlaunchesclient.Run(runCtx)and the diagnose probes (both tied to the samerunCtxsoClose()cancels them).Close()callsgoose.Client.Shutdownfirst to send graceful RSTs, then cancelsrunCtxto stop the workers.uot.Clientwhenudp_over_tcp.enabledis set, using the outbound itself as the underlying TCP dialer.Commits
Self-contained, ordered to minimize churn in
diagnoseAndMarkReady:2d8f97b3feat: add GooseRelay outbound under protocol/hiddify/gooserelay6bc48c0bstyle(constant): re-align//Hcomment column after adding TypeGooseRelayc7cf64d6refactor(gooserelay): renamedefaultHandshakeBudget→defaultDiagnoseTimeout8bf7a0a1fix(gooserelay): checktunnel_keylength before attempting hex decode25a163defix(gooserelay): rejectscript_keyscontaining URL separators1a01efb5refactor(gooserelay): use context-aware logger methods63f71e49fix(gooserelay): tie diagnose probe to runCtx so Close cancels itfbd73fb0feat(gooserelay): probe allscript_keysconcurrently, ready on first pass71acba52test(gooserelay): add option-validation table testsa73acd51refactor(gooserelay): switch to upstream's goose SDK packageCommits 2–10 are review-fixes/migration on top of commit 1. Happy to squash on merge if preferred.
Test plan
go build ./...— passes (only the unrelated macOS-lobjclinker warning).go test -race ./protocol/hiddify/gooserelay/...— passes (10 sub-cases inTestNew_OptionValidationcovering missing/invalidscript_keysandtunnel_key, URL-separator rejection, and the happy path).go vet ./protocol/hiddify/gooserelay/...— clean.cmd/sing-box, ransing-box check -c <config>against:gooserelayconfig → exits 0 ✓tunnel_keylength 8 →tunnel_key must be 64 hex characters (AES-256)✓script_keys[0] = "abc/def"→URL separator (/?#); paste the deployment ID only✓script_keys→script_keys is required✓cmd/serverof the fork on a VPS, then pointing realscript_keys+tunnel_keyat it. The outbound is structurally complete; this is just a deployment step the operator runs.Reviewer notes
hiddifyafk/hiddify-sing-boxbecausehiddifyafklacks push access on the org repo (same constraint hit on the GooseRelayVPN fork). Mirroring the workflow used for PR update to latest #1/new: add KDE set system proxy support #2 there.ProxyDisplayName:GooseRelay— single-token style matchingDNSTT.goose/sdk.gowrapper currently uses type aliases (type Config = carrier.Config), which keeps us coupled to upstream's struct shape. If upstream renames aConfigfield, this PR's outbound code may need to follow. Concrete-struct wrappers would insulate us further; deferred until upstream signals instability.🤖 Generated with Claude Code