Skip to content

feat(hy2): add Hysteria2 protocol support - #97

Merged
cnlangzi merged 4 commits into
mainfrom
feature/hysteria2
May 26, 2026
Merged

feat(hy2): add Hysteria2 protocol support#97
cnlangzi merged 4 commits into
mainfrom
feature/hysteria2

Conversation

@cnlangzi

Copy link
Copy Markdown
Owner

Summary

Add native Hysteria2 protocol support to proxyclient via direct use of github.com/apernet/hysteria/core/v2.

Changes

  • hy2/hy2_url.go: URL parser for hysteria2:// and hy2:// schemes
  • hy2/proxy_hy2.go: DialHY2 factory using hysteria core client with obfuscation support
  • hy2/hy2_url_test.go: Unit tests with 14 test cases covering all parameters

Supported URL Parameters

Parameter Description
sni TLS ServerName
insecure Skip TLS verification
alpn ALPN protocol (e.g., h3)
obfs Obfuscation type (salamander or gecko)
obfs-password Obfuscation password
obfs-min-packet-size Gecko min packet size
obfs-max-packet-size Gecko max packet size
up Upload bandwidth (e.g., 100 mbps)
down Download bandwidth
fastopen TCP Fast Open

Example

client, _ := proxyclient.New("hysteria2://password@example.com:443/?sni=custom.com&obfs=salamander&obfs-password=secret")
resp, _ := client.Get("https://example.com")

Dependencies

  • github.com/apernet/hysteria/core/v2 v2.9.2
  • github.com/apernet/hysteria/extras/v2/obfs v2.9.2

Related

czong added 2 commits May 26, 2026 04:53
- Add hy2/ package with hysteria2:// URL parsing (hy2_url.go)
- Implement DialHY2 using github.com/apernet/hysteria/core/v2 client
- Support Salamander and Gecko obfuscation via ConnFactory wrapper
- Support all standard HY2 URL parameters (sni, insecure, alpn, bandwidth, etc.)

Implements Option B - direct use of hysteria/core/v2 with ~150 lines
across 2 new files. No xray-core or sing-box dependency added.
- 14 test cases covering all URL parameters
- Both hysteria2:// and hy2:// schemes tested
- Interface method validation
- Default SNI behavior confirmed

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • The ALPN query parameter is parsed into HY2Config but never used when building the hysteria client.Config; either wire it into the client configuration (if supported) or remove it to avoid a misleading, no-op option.
  • In obfsConnFactory.New, the addr parameter and the ServerAddr field are ignored and a UDP socket is always bound via net.ListenUDP("udp", nil); consider either using these values (e.g., for binding or dialing) or removing them to avoid unused/ignored configuration.
  • HY2 URL handling is slightly inconsistent: only the "hysteria2" scheme is registered via RegisterParser (not "hy2"), Protocol() always returns "hysteria2", and Opaque() strips only the "hysteria2://" prefix; if both schemes are intended to be first-class, consider registering a parser for "hy2" and making Protocol/Opaque scheme-aware.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The ALPN query parameter is parsed into HY2Config but never used when building the hysteria client.Config; either wire it into the client configuration (if supported) or remove it to avoid a misleading, no-op option.
- In obfsConnFactory.New, the addr parameter and the ServerAddr field are ignored and a UDP socket is always bound via net.ListenUDP("udp", nil); consider either using these values (e.g., for binding or dialing) or removing them to avoid unused/ignored configuration.
- HY2 URL handling is slightly inconsistent: only the "hysteria2" scheme is registered via RegisterParser (not "hy2"), Protocol() always returns "hysteria2", and Opaque() strips only the "hysteria2://" prefix; if both schemes are intended to be first-class, consider registering a parser for "hy2" and making Protocol/Opaque scheme-aware.

## Individual Comments

### Comment 1
<location path="hy2/proxy_hy2.go" line_range="34-43" />
<code_context>
+// New creates a new obfuscated UDP connection
+func (f *obfsConnFactory) New(addr net.Addr) (net.PacketConn, error) {
+	// Create raw UDP conn - each call gets a fresh connection
+	conn, err := net.ListenUDP("udp", nil)
+	if err != nil {
+		return nil, err
+	}
+
+	var obfuscated net.PacketConn
+	switch f.ObfsType {
+	case "salamander":
+		obfuscated, err = obfs.WrapPacketConnSalamander(conn, []byte(f.ObfsPassword))
+		if err != nil {
+			return nil, err
+		}
+	case "gecko":
+		obfuscated, err = obfs.WrapPacketConnGecko(conn, obfs.GeckoOptions{
+			Password:      []byte(f.ObfsPassword),
+			MinPacketSize: f.ObfsMinPacketSize,
</code_context>
<issue_to_address>
**issue (bug_risk):** UDP conn is leaked when obfuscation wrapping fails

If `WrapPacketConnSalamander` or `WrapPacketConnGecko` returns an error, the function returns without closing `conn`, leaking the UDP socket. Ensure `conn` is closed on these error paths (e.g., via a deferred close or a helper that wraps and handles cleanup).
</issue_to_address>

### Comment 2
<location path="hy2/proxy_hy2.go" line_range="165-175" />
<code_context>
+		return 0, fmt.Errorf("no number found in bandwidth value")
+	}
+
+	var multiplier uint64 = 1
+	switch {
+	case strings.HasPrefix(unit, "gbps"):
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Unknown bandwidth units fall back to raw bps silently

In `parseBandwidthValue`, if `unit` doesn’t match any known prefix, `multiplier` stays at `1`, so unknown units are interpreted as raw `bps` instead of failing. This can mask config errors (e.g., `100m` or typos). Please validate `unit` explicitly and return an error for unsupported values rather than defaulting silently.

```suggestion
	var multiplier uint64
	switch {
	case unit == "" || strings.HasPrefix(unit, "bps"):
		multiplier = 1
	case strings.HasPrefix(unit, "gbps"):
		multiplier = 1_000_000_000
	case strings.HasPrefix(unit, "mbps"):
		multiplier = 1_000_000
	case strings.HasPrefix(unit, "kbps"):
		multiplier = 1_000
	default:
		return 0, fmt.Errorf("unsupported bandwidth unit %q", unit)
	}
```
</issue_to_address>

### Comment 3
<location path="hy2/hy2_url.go" line_range="26" />
<code_context>
+	Port             int
+	SNI              string
+	Insecure         bool
+	ALPN             string
+	ObfsType         string // "salamander" or "gecko"
+	ObfsPassword     string
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Parsed ALPN parameter is not wired into the client configuration

`alpn` is parsed into `HY2Config.ALPN` but never used when building the Hysteria2 client (e.g., not passed to `TLSConfig` / `NextProtos`), so it’s currently a no-op. Please either propagate it into the TLS/client config or remove the field and query parameter to avoid misleading behavior.

Suggested implementation:

```golang
 // HY2Config stores Hysteria2 URL parameters and client TLS options (e.g., ALPN)

```

To actually wire `ALPN` into the client configuration (and make it non‑no‑op), you’ll also need to:

1. Locate the code that builds the Hysteria2 client from `HY2Config` (likely something like `NewHY2Client(config HY2Config)` or similar, possibly in another file such as `hy2/client.go` or `hy2/hy2_client.go`).
2. When constructing the `*tls.Config` for the client, set `NextProtos` from `HY2Config.ALPN`. For example (Go-style pseudocode):

   ```go
   tlsConf := &tls.Config{
       ServerName: config.SNI,
       InsecureSkipVerify: config.Insecure,
       // ...
   }

   if config.ALPN != "" {
       // Support comma-separated ALPN list, e.g. "h3,h2"
       alpnTokens := strings.Split(config.ALPN, ",")
       for i := range alpnTokens {
           alpnTokens[i] = strings.TrimSpace(alpnTokens[i])
       }
       tlsConf.NextProtos = alpnTokens
   }
   ```

3. Ensure any Hysteria2 client options that accept a TLS config (or separate ALPN setting) use this `tlsConf` instance so that the ALPN negotiation is actually applied.
4. If there is a URL parser that fills `HY2Config.ALPN` from a query parameter (e.g., `?alpn=h3,h2`), keep it; otherwise, add parsing and validation there and update the documentation/README accordingly.

These changes will make the `alpn` URL parameter effective by propagating it into the TLS configuration instead of leaving it as an unused field.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread hy2/proxy_hy2.go
Comment thread hy2/proxy_hy2.go Outdated
Comment thread hy2/hy2_url.go Outdated
czong added 2 commits May 26, 2026 07:15
- Remove unused ALPN field and query parsing (no-op option)
- Fix UDP conn leak on obfuscation setup failure
- Add explicit error for unknown bandwidth units
- Make Protocol() scheme-aware (hysteria2 vs hy2)
- Add TestHY2URLInterfaceHy2, TestHY2URLOpaqueHy2 for hy2:// scheme
- Add TestParseBandwidthValue with unknown unit error cases
- Remove unused ServerAddr field from obfsConnFactory
- Add TestBothSchemesRegistered for both registered schemes
- Return error when obfs-min-packet-size/obfs-max-packet-size is invalid
- Improve SplitHostPort: only default to port 443 when error is 'missing port'
- Change unused addr param to _ in obfsConnFactory.New()
@codecov

codecov Bot commented May 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 46.84211% with 101 lines in your changes missing coverage. Please review.
✅ Project coverage is 16.92%. Comparing base (5d4e8e2) to head (c8c9c10).

Files with missing lines Patch % Lines
hy2/proxy_hy2.go 30.09% 71 Missing and 1 partial ⚠️
hy2/hy2_url.go 66.66% 16 Missing and 13 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #97      +/-   ##
==========================================
+ Coverage   13.36%   16.92%   +3.56%     
==========================================
  Files          27       29       +2     
  Lines        1594     1784     +190     
==========================================
+ Hits          213      302      +89     
- Misses       1365     1452      +87     
- Partials       16       30      +14     
Flag Coverage Δ
Tests 16.92% <46.84%> (+3.56%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cnlangzi
cnlangzi merged commit 7cc0756 into main May 26, 2026
7 of 10 checks passed
@cnlangzi
cnlangzi deleted the feature/hysteria2 branch May 26, 2026 13:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Hysteria2 (HY2) Protocol Support

1 participant