Documentation
¶
Overview ¶
Package relay provides decentralized peer-to-peer traffic forwarding through Hysteria 2 tunnels.
Every node is equal — each can run both an hy2 server (accepting peers) and multiple hy2 clients (connecting to peers). Peers can route traffic through each other's network.
Peer discovery is local by default (only directly connected peers). Nested discovery (seeing a peer's peers) is opt-in per peer.
Index ¶
- Constants
- Variables
- func BuildViaAddr(remaining, addr string) string
- func IsRelayStream(addr string) bool
- func ParseBridgeAddr(addr string) (string, string, bool)
- func ParseVia(reqAddr string) (chain, targetAddr string, ok bool)
- type BidirectionalIdleTrigger
- type ConnState
- type IPStatus
- type LatencySample
- type Node
- func (n *Node) AddPeerConn(name string, client hyclient.Client, addr, status string)
- func (n *Node) AttachNative(ctx context.Context, peerName string, client hyclient.Client) error
- func (n *Node) AttachTo(ctx context.Context, peerName string, client hyclient.Client, ...) error
- func (n *Node) BlockPeer(name string)
- func (n *Node) Bridges() *bridgeManager
- func (n *Node) ConnectedPeerNames() []string
- func (n *Node) DialIPTun(ctx context.Context, peerName string) (net.Conn, error)
- func (n *Node) DialTCP(ctx context.Context, peerName string, addr string) (net.Conn, error)
- func (n *Node) DialTCPBridged(peerName, addr string) (net.Conn, string, error)
- func (n *Node) DialTCPIdx(ctx context.Context, peerName string, addr string, clientIdx int) (net.Conn, error)
- func (n *Node) DialUDP(ctx context.Context, peerName string, addr string) (net.Conn, error)
- func (n *Node) DialUDPVia(ctx context.Context, path []string, addr string) (net.Conn, error)
- func (n *Node) DialVia(ctx context.Context, path []string, addr string) (net.Conn, error)
- func (n *Node) DialViaBridged(ctx context.Context, path []string, addr string) (net.Conn, string, error)
- func (n *Node) DisconnectPeer(name string)
- func (n *Node) GetAddrLatencies(peerName string) map[string]int
- func (n *Node) GetLatency(peerName string) int
- func (n *Node) GetStats() Stats
- func (n *Node) HandleStream(ctx context.Context, reqAddr string, stream net.Conn)
- func (n *Node) HasPeer(name string) bool
- func (n *Node) HealthSnapshots() []PeerHealthSnapshot
- func (n *Node) IsExit() bool
- func (n *Node) IsInbound(name string) bool
- func (n *Node) IsNestedEnabled(peerName string) bool
- func (n *Node) IsPeerBlocked(name string) bool
- func (n *Node) IsPeerTunCapable(peerName string) bool
- func (n *Node) LatencySnapshots() []PeerLatencySnapshot
- func (n *Node) Name() string
- func (n *Node) NativeMap() map[string]bool
- func (n *Node) NodeID() string
- func (n *Node) PathRates() map[string]PeerTraffic
- func (n *Node) PeerConflicts() map[string]string
- func (n *Node) PeerConnCount(name string) int
- func (n *Node) PeerCtx(peerName string) context.Context
- func (n *Node) PeerIPStatuses(name string) []IPStatus
- func (n *Node) PeerRates() map[string]PeerTraffic
- func (n *Node) Peers() []PeerInfo
- func (n *Node) PeersAlt() []PeerInfo
- func (n *Node) PeersOf(peerName string) ([]PeerInfo, error)
- func (n *Node) PeersOfCacheKeys() []string
- func (n *Node) PeersOfCached(peerName string) ([]PeerInfo, bool)
- func (n *Node) PeersOfRaw(peerName string) ([]byte, error)
- func (n *Node) PeersOfVia(ctx context.Context, path []string) ([]PeerInfo, error)
- func (n *Node) PingPeer(name string) time.Duration
- func (n *Node) RebindSnapshots() []PeerRebindSnapshot
- func (n *Node) SetAPIHandler(handler func(stream net.Conn))
- func (n *Node) SetAPIStreamWrap(fn func(stream net.Conn, authID string) net.Conn)
- func (n *Node) SetExit(exit bool)
- func (n *Node) SetIPTunHandler(handler func(peerName string, stream net.Conn))
- func (n *Node) SetLatency(peerName string, ms int)
- func (n *Node) SetListPeersFunc(fn func() []byte)
- func (n *Node) SetName(name string)
- func (n *Node) SetNestedDiscovery(peerName string, enabled bool)
- func (n *Node) SetNodeID(id string)
- func (n *Node) SetOnInboundIdentity(handler func(declaredName, nodeID, remoteAddrIP string))
- func (n *Node) SetPeerConflict(clientAddr string, conflictName string)
- func (n *Node) SetPeersOfCache(peerName string, peers []PeerInfo)
- func (n *Node) StartLatencyProber(ctx context.Context)
- func (n *Node) StartRateTicker(ctx context.Context)
- func (n *Node) UnblockPeer(name string)
- type PeerHealthSnapshot
- type PeerInfo
- type PeerLatencySnapshot
- type PeerRebindSnapshot
- type PeerTraffic
- type RebindOpts
- type RebindTrigger
- type Stats
Constants ¶
const LatencyHistoryCap = 120
LatencyHistoryCap bounds the per-peer ring. With ~5 s probe cadence this gives ~10 minutes of history per peer, ~1.5 KB/peer at most — trivial even with dozens of peers.
const MinCompatVersion = "1.3.0"
MinCompatVersion is the minimum peer version we can work with. Peers below this version are marked incompatible: relay blocked, nested disabled.
const ProtocolVersion = 3
ProtocolVersion is an integer identifying the inter-node wire format generation. Bumped (independently of NodeVersion semver) whenever a breaking peer-protocol change ships. Older instances that see a remote peer with a HIGHER ProtocolVersion treat it as opaque: hide nested sub-peers entirely (they may use frames we can't parse) and mark direct peers as unsupported. Newer instances accept any older peer and just downgrade behavior locally to the older feature set. v1.3.x ships protocol v3 — v1 = pre-1.0, v2 = 1.0–1.2.
const ( // StreamAPI is the well-known relay address the web UI's remote proxy // uses to reach a peer's own API server. It bypasses the normal // loopback-rewrite logic (which would redirect 127.0.0.1 to the Docker // gateway and break this case). StreamAPI = "_relay_api_:0" )
Variables ¶
var ErrNotHy2scale = fmt.Errorf("relay: remote is not hy2scale")
ErrNotHy2scale indicates the remote is a plain hy2 server without relay protocol.
var NodeTunCapable bool
NodeTunCapable is set by the app layer at startup if this node can handle exit-side TUN (has NET_ADMIN + /dev/net/tun).
var NodeVersion = "1.0.0"
NodeVersion is the version string sent during peer registration. Set by the app package at init time.
Functions ¶
func BuildViaAddr ¶ added in v1.3.6
BuildViaAddr constructs a via-encoded relay address used as the "target" passed to a first-hop DialUDP / DialTCP. `remaining` is the slash-joined chain of hops AFTER the immediate first hop; `addr` is the final target ("host:port"). The first hop's handler parses with ParseVia and forwards down the chain.
func IsRelayStream ¶
IsRelayStream returns true if addr is a relay internal stream.
func ParseBridgeAddr ¶ added in v1.3.0
HandleBridgeAddr processes a bridge-tagged address from the exit node. Returns (targetAddr, bridgeID, isBridged).
func ParseVia ¶ added in v1.3.6
ParseVia decodes a via-encoded multi-hop relay target address. Returns (chain, targetAddr, true) when reqAddr has the via-prefix. `chain` is the slash-joined remaining hops (e.g. "aub/cn/2400"); `targetAddr` is the final destination ("host:port"). The receiving peer should split `chain` on "/" and forward via its first hop with the remainder re-encoded. Exported so the application's UDP outbound callback (nodeOutbound.UDP in internal/app/app.go) can implement multi-hop UDP routing, mirroring TCP's handleVia.
Types ¶
type BidirectionalIdleTrigger ¶ added in v1.3.1
BidirectionalIdleTrigger fires when *both* directions have been silent for Timeout. Replaces the old "no Read while Writing" rule, which wrongly fired during legitimate one-way uploads (e.g. HTTP POST while the server hadn't responded yet) and slow downloads. Bidirectional silence remains a strong signal of a truly stuck stream — the same signal the cn-xinchang storm fixes were aimed at.
func (BidirectionalIdleTrigger) ShouldRebind ¶ added in v1.3.1
func (t BidirectionalIdleTrigger) ShouldRebind(s ConnState) (bool, string)
ShouldRebind implements RebindTrigger.
type ConnState ¶ added in v1.3.1
type ConnState struct {
LastRead int64
LastWrite int64
ViaPath []string // current via path; len==0 means direct (single-hop bridge)
}
ConnState captures the metrics a RebindTrigger evaluates. Values are unix nanoseconds; zero means "no activity yet".
type IPStatus ¶ added in v1.2.0
type IPStatus struct {
Addr string `json:"addr"`
Status string `json:"status"` // "online", "offline", "mismatch", "native"
LatencyMs int `json:"latency_ms,omitempty"`
}
IPStatus represents per-IP connection info.
type LatencySample ¶ added in v1.3.4
LatencySample is one entry in the per-peer time series. ms < 0 means the probe failed; the operator can render those as gaps. Capped to LatencyHistoryCap entries — older samples slide off the head.
type Node ¶
type Node struct {
// contains filtered or unexported fields
}
func (*Node) AddPeerConn ¶ added in v1.2.0
AddPeerConn adds an extra QUIC connection to an existing peer.
func (*Node) AttachNative ¶
AttachTo connects to a remote node via an hy2 client, registers, and handles dial requests from the remote. Blocks until ctx or connection ends. AttachTo connects to a remote node via an hy2 client, registers, and handles dial requests from the remote. Blocks until ctx or connection ends. Returns the remote's actual node ID via the onID callback (if non-nil). AttachNative registers a plain hy2 server (no relay protocol) as a peer. Only DialTCP works (direct proxy). No nested discovery, no control stream.
func (*Node) BlockPeer ¶ added in v1.2.2
BlockPeer prevents a peer from registering (inbound connections rejected).
func (*Node) Bridges ¶ added in v1.3.0
func (n *Node) Bridges() *bridgeManager
PeerCtx returns the context for a peer, canceled when the peer disconnects. Used by idle timeout to detect dead relay streams immediately. Bridges returns the bridge manager for stream rebinding.
func (*Node) ConnectedPeerNames ¶
ConnectedPeerNames returns names of all connected peers (outbound with active client).
func (*Node) DialIPTun ¶ added in v1.2.2
DialIPTun opens a bidirectional IP packet tunnel stream to a peer. The stream uses 2-byte length-prefixed framing for raw IP packets.
func (*Node) DialTCPBridged ¶ added in v1.3.0
DialTCPBridged is like DialTCP but returns a connection that survives QUIC reconnects by rebinding to new streams transparently.
func (*Node) DialTCPIdx ¶ added in v1.3.0
func (n *Node) DialTCPIdx(ctx context.Context, peerName string, addr string, clientIdx int) (net.Conn, error)
DialTCPIdx dials TCP through a specific QUIC client index (for bond path pinning). Index -1 means round-robin (same as DialTCP).
func (*Node) DialUDP ¶ added in v1.2.2
DialTCP dials addr through a directly connected peer's network. DialUDP opens a UDP session through a peer's Hysteria2 QUIC connection. Returns a net.Conn-compatible wrapper for a single destination address.
func (*Node) DialUDPVia ¶ added in v1.3.6
DialUDPVia opens a multi-hop UDP relay connection. `path` is the chain of peer names [hop1, hop2, ..., exit]; `addr` is the final destination ("host:port"). For single-hop, this is DialUDP. For multi-hop, the target address is via-encoded and each intermediate hop's nodeOutbound.UDP re-encodes and forwards to the next hop — the UDP analog of TCP's DialVia.
func (*Node) DialVia ¶
DialVia dials addr through a chain of peers. path = ["au", "jp"] means: this node → au → jp → internet.
func (*Node) DialViaBridged ¶ added in v1.3.0
func (n *Node) DialViaBridged(ctx context.Context, path []string, addr string) (net.Conn, string, error)
DialViaBridged dials addr through a chain of peers with bridge support. Returns a bridgedConn that survives first-hop QUIC reconnects.
func (*Node) DisconnectPeer ¶ added in v1.2.2
DisconnectPeer forcibly disconnects a peer (both inbound and outbound). Closes the QUIC client to kill all active streams through this peer, ensuring no connections hang in a black hole after reconnection.
func (*Node) GetAddrLatencies ¶ added in v1.3.0
GetAddrLatencies returns per-address latency for a multi-addr peer.
func (*Node) GetLatency ¶
GetLatency returns stored latency for a peer. For multi-addr peers, returns average of all addr latencies. -1 if unknown.
func (*Node) HandleStream ¶
HandleStream routes relay streams from the hy2 server's Outbound.TCP.
func (*Node) HealthSnapshots ¶ added in v1.3.4
func (n *Node) HealthSnapshots() []PeerHealthSnapshot
HealthSnapshots returns the QUIC-health proxy axis for every peer.
func (*Node) IsInbound ¶ added in v1.0.5
IsInbound returns true if the peer is connected inbound (no client).
func (*Node) IsNestedEnabled ¶
IsNestedEnabled returns whether nested discovery is enabled for a peer.
func (*Node) IsPeerBlocked ¶ added in v1.2.2
IsPeerBlocked checks if a peer is blocked.
func (*Node) IsPeerTunCapable ¶ added in v1.3.0
IsPeerTunCapable checks if a peer supports exit-side TUN. For multi-hop paths ("A/B/C") it walks each segment: first hop must be a direct peer, then each subsequent segment is looked up in the sub-peer cache under the qualified parent path ("A", then "A/B", ...). This disambiguates cases where a nested peer shares a name with a direct peer of the local node (e.g. HUB has a direct "2400" AND another "2400" exists as a child of some intermediate peer).
func (*Node) LatencySnapshots ¶ added in v1.3.4
func (n *Node) LatencySnapshots() []PeerLatencySnapshot
LatencySnapshots returns the latency-time-series axis for every peer. Includes the current point value (matching what the topology API surfaces) plus the rolling history ring.
func (*Node) NodeID ¶ added in v1.3.1
NodeID returns the local stable identifier configured via SetNodeID.
func (*Node) PathRates ¶ added in v1.3.0
func (n *Node) PathRates() map[string]PeerTraffic
PathRates returns per-qualified-path traffic rates, keyed identically to cfg.Peers (e.g. "au", "au/au-r1", "au/au-r1/au-r1-a"). Only paths with more than one hop are populated — single-hop flows are covered by the per-peer counter returned from PeerRates.
func (*Node) PeerConflicts ¶ added in v1.3.0
PeerConflicts returns a map of client addr → conflicting name.
func (*Node) PeerConnCount ¶ added in v1.2.0
PeerConnCount returns the number of QUIC connections to a peer.
func (*Node) PeerIPStatuses ¶ added in v1.2.0
PeerIPStatuses returns per-IP status for a peer.
func (*Node) PeerRates ¶
func (n *Node) PeerRates() map[string]PeerTraffic
PeerRates returns per-peer traffic rates.
func (*Node) PeersAlt ¶ added in v1.3.1
PeersAlt returns a snapshot of the secondary-slot peer infos. Pairs with Peers() for the primary side. Topology API merges the two so a mutually-dialed peer surfaces as both top-level outbound and inbound child of self in the rendered tree.
func (*Node) PeersOfCacheKeys ¶ added in v1.3.8
PeersOfCacheKeys snapshots every qualified-path key currently held in peersOfCache (both bare direct-peer names and the slash-joined deep keys the api server's walkAndCache populates). Callers that want to enumerate the full set of topology-visible paths beyond cfg.Peers — e.g. the PathInfoExpand history recorder, which used to miss any qualified path the operator hadn't explicitly toggled even though the topology graph rendered it — read this list and union it with their own roots.
func (*Node) PeersOfCached ¶
PeersOfCached returns cached peer list from the last prober cycle.
func (*Node) PeersOfRaw ¶ added in v1.3.0
PeersOfRaw returns the raw JSON from a peer's streamListPeers response. Used by the API server to get rich tree data (with children).
func (*Node) PeersOfVia ¶
PeersOfVia returns a peer's peers through a multi-hop path. path = ["vm", "au"] means: query au's peers by routing through vm.
func (*Node) PingPeer ¶
PingPeer measures round-trip latency to a peer. Returns -1 if unreachable. Non-blocking: uses a 3-second timeout.
func (*Node) RebindSnapshots ¶ added in v1.3.4
func (n *Node) RebindSnapshots() []PeerRebindSnapshot
RebindSnapshots returns the rebind-check counter axis for every peer.
func (*Node) SetAPIHandler ¶ added in v1.3.0
SetAPIHandler registers a handler for incoming _relay_api_ streams (remote web-UI proxy tunneling).
func (*Node) SetAPIStreamWrap ¶ added in v1.3.7
SetAPIStreamWrap registers the app-layer wrapper that tags a peer-relay API stream with its auth ID so the api server can apply the RelayAdminPassthrough bypass. Required so streams that arrive via the s2c-ctrl dial-request path (inbound-only peer) carry the same identification the direct nodeOutbound.TCP path already wraps for.
func (*Node) SetIPTunHandler ¶ added in v1.2.2
SetIPTunHandler registers a handler for incoming IP tunnel streams from peers.
func (*Node) SetLatency ¶
SetLatency stores a latency measurement for a peer (called by prober or inbound report).
func (*Node) SetListPeersFunc ¶ added in v1.3.0
SetListPeersFunc registers a function that returns rich peer list JSON (with children). Used by handleListPeers so both outbound and inbound nested discovery return identical data.
func (*Node) SetNestedDiscovery ¶
SetNestedDiscovery enables/disables nested peer discovery for a peer.
func (*Node) SetNodeID ¶ added in v1.3.1
SetNodeID is called by the app layer at startup (and whenever the NodeID is regenerated/rebound) so register handshakes can transmit our stable identifier. Empty until set — keep the first call early in App init, before any connectLoop fires.
func (*Node) SetOnInboundIdentity ¶ added in v1.3.6
SetOnInboundIdentity registers a callback that fires when an inbound peer completes its register-stream meta exchange. The app layer uses this to detect when an inbound peer that previously connected from the same source IP now reports a different NodeID, and to propagate the rename through cfg.Peers / cfg.Proxies.ExitVia / cfg.Users.ExitVia (the inbound counterpart of AttachTo's outbound rename). See docs/peer-identity-sync-bugs.md Bug B.
func (*Node) SetPeerConflict ¶ added in v1.3.0
SetPeerConflict records a name conflict for a client address. The conflict is tracked separately from the peer entry to avoid marking legitimate peers as conflicting.
func (*Node) SetPeersOfCache ¶ added in v1.2.0
SetPeersOfCache sets the cached peer list for a given peer name. Called by the API sub-peers updater to share nested topology data.
func (*Node) StartLatencyProber ¶
StartLatencyProber periodically pings outbound peers and caches their peer lists.
func (*Node) StartRateTicker ¶
StartRateTicker updates per-second rate counters. Call in a goroutine.
func (*Node) UnblockPeer ¶ added in v1.2.2
UnblockPeer allows a peer to register again.
type PeerHealthSnapshot ¶ added in v1.3.4
type PeerHealthSnapshot struct {
Name string `json:"name"`
Direction string `json:"direction"` // "outbound" / "inbound"
PingOK uint64 `json:"ping_ok"`
PingFail uint64 `json:"ping_fail"`
FailCount int32 `json:"fail_count"` // consecutive failures (existing field)
TxBytes uint64 `json:"tx_bytes"`
RxBytes uint64 `json:"rx_bytes"`
}
PeerHealthSnapshot is one row of /api/diag/peer-health (the ping-ok/fail axis — application-level proxy for QUIC-tunnel health since the hysteria `Client` interface doesn't expose raw quic-go retransmit / pacer-bandwidth fields).
type PeerInfo ¶
type PeerInfo struct {
Name string `json:"name"`
ExitNode bool `json:"exit_node"`
Direction string `json:"direction"`
Native bool `json:"native"`
LatencyMs int `json:"latency_ms"`
Version string `json:"version,omitempty"`
// NodeID is the remote's reported stable identifier (from peerMeta).
// Used by the bidirectional-slot logic to prove "this incoming
// register is from the same logical instance as the existing primary,
// not an impostor". Empty for old peers — that case falls back to
// legacy single-slot replace behavior.
NodeID string `json:"node_id,omitempty"`
Incompatible bool `json:"incompatible,omitempty"`
Conflict bool `json:"conflict,omitempty"`
TunCapable bool `json:"tun_capable,omitempty"`
// PV is the remote's wire-protocol generation. Higher than ours
// means the remote may speak frames we can't parse — see
// ProtocolVersion comment. Forwarded to the topology API so the
// frontend can hide nested sub-peers from such links and render
// the direct connection itself as `unsupported`.
PV int `json:"pv,omitempty"`
Unsupported bool `json:"unsupported,omitempty"`
}
PeerInfo describes a connected peer.
type PeerLatencySnapshot ¶ added in v1.3.4
type PeerLatencySnapshot struct {
Name string `json:"name"`
Direction string `json:"direction"`
LatencyMs int `json:"latency_ms"` // most recent point value
History []LatencySample `json:"history"`
}
PeerLatencySnapshot is one row of /api/diag/peer-latency — the rolling time series of recent latency samples per peer. Ring is LatencyHistoryCap entries (~10 min at 5 s probe cadence).
type PeerRebindSnapshot ¶ added in v1.3.4
type PeerRebindSnapshot struct {
Name string `json:"name"`
Direction string `json:"direction"`
RebindChecks uint64 `json:"rebind_checks"`
}
PeerRebindSnapshot is one row of /api/diag/peer-rebinds — counts of [via] rebind-probe arrivals at this peer's tunnel since session start. Spikes correlate with chronic per-stream disruption on a QUIC connection that hasn't fully dropped.
type PeerTraffic ¶
PeerTraffic holds per-peer traffic info.
type RebindOpts ¶ added in v1.3.1
type RebindOpts struct {
ViaPath []string // if non-nil, replace bridgedConn.viaPath for this rebind
}
RebindOpts allows callers to override defaults when explicitly invoking rebind. Currently only the via path can be overridden — left as the sole member so the type is the obvious extension point for future cross-path migration work.
type RebindTrigger ¶ added in v1.3.1
RebindTrigger decides whether a bridged stream should be torn down and rebound. Triggers run on the writer hot path; ShouldRebind must be O(1). Implementations may consider any field of ConnState; new fields will be added without breaking compatibility.
type Stats ¶
type Stats struct {
TxBytes uint64 // total uploaded
RxBytes uint64 // total downloaded
TxRate uint64 // bytes/sec upload (snapshot)
RxRate uint64 // bytes/sec download (snapshot)
Conns int // active stream count
ExitClients int // peers using this node as exit
}
Node is a unified relay endpoint. It can accept peers (hy2 server side) and connect to peers (hy2 client side) simultaneously. Stats holds traffic counters.