Skip to content

hysteria2: Fix obfs silently disabling socket buffer sizing - #17

Open
realyxl wants to merge 15 commits into
SagerNet:devfrom
realyxl:hysteria2-obfs-socket-buffer
Open

hysteria2: Fix obfs silently disabling socket buffer sizing#17
realyxl wants to merge 15 commits into
SagerNet:devfrom
realyxl:hysteria2-obfs-socket-buffer

Conversation

@realyxl

@realyxl realyxl commented Aug 1, 2026

Copy link
Copy Markdown

SalamanderPacketConn and GeckoPacketConn embed the net.PacketConn interface, so SetReadBuffer/SetWriteBuffer are not promoted. quic-go gives up on its first assertion:

// sys_conn_buffers.go
conn, ok := c.(interface{ SetReadBuffer(int) error })
if !ok {
    return errors.New("connection doesn't allow setting of receive buffer size. Not a *net.UDPConn?")
}

No syscall is ever issued, so with obfs configured the socket stays at net.core.rmem_default (212992, ~146 packets of depth) instead of quic-go's 8 MB and overflows in bursts. Without obfs the conn arrives as a bare *net.UDPConn and sizing works, which is why the symptom only shows with obfuscation on. Upstream hysteria forwards these methods for the same reason (extras/obfs/conn.go).

Both are re-exposed through common.Cast, which follows Upstream() so client-side wrappers such as bufio.UnbindPacketConn are resolved; this mirrors hysteria/hop.go. SyscallConn is deliberately left out: wrapConn aborts listen/dial when it errors, and the client may run over a non-UDP conn through a detour dialer.

Reproducing quic-go's assertions against a real socket:

SetReadBuffer OOBCapablePacketConn SO_RCVBUF SO_SNDBUF
*net.UDPConn (reference) true true 786896 → 1048576 9216 → 1048576
salamander / gecko, before false false 786896 → 786896 9216 → 9216
salamander / gecko, after true false 786896 → 1048576 9216 → 1048576

OOBCapablePacketConn stays false, so quic-go keeps using ReadFrom/WriteTo and never bypasses deobfuscation. Over a non-UDP conn both return os.ErrInvalid, which wrapConn discards.

Builds and vets clean on linux, android, windows, darwin and freebsd; gofumpt, gofmt -s and gci report no changes.

The salamander.go half applies unchanged to main if you want it on the 1.13.x line.

nekohasekai and others added 15 commits July 27, 2026 11:09
Discover/Punch now take []netip.AddrPort and stay family-agnostic; the
caller resolves STUN servers once and filters per-family at the call
site. Drops conn.LocalAddr() v4/v6 inference, which was ambiguous on
dual-stack or unbound sockets and redundant now that the client and
server already know which family each socket belongs to.
Options.IPVersion (4 or 6) restricts realm connections to a single IP
family: the client only opens the matching family conn, and the server
only resolves STUN servers and punches peer candidates of that family.
Optional gateway port mapping established before STUN discovery, so in a
double-NAT setup the address STUN observes corresponds to the path
through the static pinhole. The mapped external address is merged into
published and connect addresses; the mapping is renewed at half its
lease lifetime and removed when the owning connection closes.
SalamanderPacketConn and GeckoPacketConn embed the net.PacketConn
interface, which hides SetReadBuffer/SetWriteBuffer. quic-go's type
assertion in setReceiveBuffer/setSendBuffer then fails and the socket
keeps net.core.rmem_default instead of the 8 MB it asks for.

Re-expose both methods, resolving the underlying conn with common.Cast
as hysteria/hop.go does.
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.

2 participants