diff --git a/docs/configuration/inbound/hysteria2.md b/docs/configuration/inbound/hysteria2.md index 28caae87..7b5b07a0 100644 --- a/docs/configuration/inbound/hysteria2.md +++ b/docs/configuration/inbound/hysteria2.md @@ -48,6 +48,12 @@ icon: material/alert-decagram "realm_id": "", "stun_servers": [], "stun_domain_resolver": "", // or {} + "ip_version": 0, + "port_mapping": { + "enabled": false, + "timeout": "", + "lifetime": "" + }, "http_client": {} } } @@ -237,6 +243,38 @@ Setting this option directly to a string is equivalent to setting `server` of th If empty, the default domain resolver is used. +#### realm.ip_version + +Restrict realm connections (STUN, hole punching, and the resulting QUIC path) to a single IP version. + +`4` or `6`. Both are used if empty. + +The `listen` address must be compatible with the selected version. + +#### realm.port_mapping + +Maintain a UDP port mapping on the local gateway via UPnP or NAT-PMP. + +The mapping is established before STUN discovery and improves hole-punching reliability behind gateways that support it; failures are non-fatal. + +Requires IPv4: conflicts with `"ip_version": 6`. + +#### realm.port_mapping.enabled + +Enable port mapping. + +#### realm.port_mapping.timeout + +Timeout for gateway discovery and mapping operations. + +`10s` is used by default. + +#### realm.port_mapping.lifetime + +Lease lifetime of the mapping; it is renewed at half the lifetime. + +`10m` is used by default. + #### realm.http_client HTTP client used to talk to the realm. diff --git a/docs/configuration/inbound/hysteria2.zh.md b/docs/configuration/inbound/hysteria2.zh.md index d59101b3..daf83b6f 100644 --- a/docs/configuration/inbound/hysteria2.zh.md +++ b/docs/configuration/inbound/hysteria2.zh.md @@ -48,6 +48,12 @@ icon: material/alert-decagram "realm_id": "", "stun_servers": [], "stun_domain_resolver": "", // 或 {} + "ip_version": 0, + "port_mapping": { + "enabled": false, + "timeout": "", + "lifetime": "" + }, "http_client": {} } } @@ -234,6 +240,38 @@ Realm 上的槽位标识符。 如果为空,则使用默认域名解析器。 +#### realm.ip_version + +将 realm 连接(STUN、打洞与最终的 QUIC 路径)限制为单一 IP 版本。 + +`4` 或 `6`。默认使用两者。 + +`listen` 地址必须与所选版本兼容。 + +#### realm.port_mapping + +通过 UPnP 或 NAT-PMP 在本地网关上维护 UDP 端口映射。 + +映射在 STUN 发现之前建立,可在支持的网关后提升打洞成功率;失败不影响正常流程。 + +需要 IPv4:与 `"ip_version": 6` 冲突。 + +#### realm.port_mapping.enabled + +启用端口映射。 + +#### realm.port_mapping.timeout + +网关发现与映射操作的超时。 + +默认使用 `10s`。 + +#### realm.port_mapping.lifetime + +映射的租约时长;每过一半时长续期一次。 + +默认使用 `10m`。 + #### realm.http_client 与 realm 通信使用的 HTTP 客户端。 diff --git a/docs/configuration/outbound/hysteria2.md b/docs/configuration/outbound/hysteria2.md index 1df36cfc..a0274937 100644 --- a/docs/configuration/outbound/hysteria2.md +++ b/docs/configuration/outbound/hysteria2.md @@ -43,6 +43,12 @@ "token": "", "realm_id": "", "stun_servers": [], + "ip_version": 0, + "port_mapping": { + "enabled": false, + "timeout": "", + "lifetime": "" + }, "http_client": {} }, @@ -210,6 +216,36 @@ List of STUN servers (`host` or `host:port`) used to discover this client's publ Domain names are resolved using [`domain_resolver`](/configuration/shared/dial/#domain_resolver) from Dial Fields. +#### realm.ip_version + +Restrict realm connections (STUN, hole punching, and the resulting QUIC path) to a single IP version. + +`4` or `6`. Both are used if empty. + +#### realm.port_mapping + +Maintain a UDP port mapping on the local gateway via UPnP or NAT-PMP. + +The mapping is established before STUN discovery and improves hole-punching reliability behind gateways that support it; failures are non-fatal. + +Requires IPv4: conflicts with `"ip_version": 6`. + +#### realm.port_mapping.enabled + +Enable port mapping. + +#### realm.port_mapping.timeout + +Timeout for gateway discovery and mapping operations. + +`10s` is used by default. + +#### realm.port_mapping.lifetime + +Lease lifetime of the mapping; it is renewed at half the lifetime. + +`10m` is used by default. + #### realm.http_client HTTP client used to talk to the realm. diff --git a/docs/configuration/outbound/hysteria2.zh.md b/docs/configuration/outbound/hysteria2.zh.md index 07ec96ce..0c24e2d9 100644 --- a/docs/configuration/outbound/hysteria2.zh.md +++ b/docs/configuration/outbound/hysteria2.zh.md @@ -43,6 +43,12 @@ "token": "", "realm_id": "", "stun_servers": [], + "ip_version": 0, + "port_mapping": { + "enabled": false, + "timeout": "", + "lifetime": "" + }, "http_client": {} }, @@ -208,6 +214,36 @@ Realm 的 Bearer 令牌,需与 realm 上配置的 `users[].token` 之一匹配 域名通过 [拨号字段](/zh/configuration/shared/dial/) 中的 [`domain_resolver`](/zh/configuration/shared/dial/#domain_resolver) 解析。 +#### realm.ip_version + +将 realm 连接(STUN、打洞与最终的 QUIC 路径)限制为单一 IP 版本。 + +`4` 或 `6`。默认使用两者。 + +#### realm.port_mapping + +通过 UPnP 或 NAT-PMP 在本地网关上维护 UDP 端口映射。 + +映射在 STUN 发现之前建立,可在支持的网关后提升打洞成功率;失败不影响正常流程。 + +需要 IPv4:与 `"ip_version": 6` 冲突。 + +#### realm.port_mapping.enabled + +启用端口映射。 + +#### realm.port_mapping.timeout + +网关发现与映射操作的超时。 + +默认使用 `10s`。 + +#### realm.port_mapping.lifetime + +映射的租约时长;每过一半时长续期一次。 + +默认使用 `10m`。 + #### realm.http_client 与 realm 通信使用的 HTTP 客户端。 diff --git a/option/hysteria2.go b/option/hysteria2.go index d20c0bd3..fdf0d2cd 100644 --- a/option/hysteria2.go +++ b/option/hysteria2.go @@ -30,9 +30,17 @@ type Hysteria2Realm struct { Token string `json:"token,omitempty"` RealmID string `json:"realm_id"` STUNServers badoption.Listable[string] `json:"stun_servers"` + IPVersion int `json:"ip_version,omitempty"` + PortMapping *Hysteria2RealmPortMapping `json:"port_mapping,omitempty"` HTTPClient *HTTPClientOptions `json:"http_client,omitempty"` } +type Hysteria2RealmPortMapping struct { + Enabled bool `json:"enabled,omitempty"` + Timeout badoption.Duration `json:"timeout,omitempty"` + Lifetime badoption.Duration `json:"lifetime,omitempty"` +} + type Hysteria2InboundRealm struct { Hysteria2Realm STUNDomainResolver *DomainResolveOptions `json:"stun_domain_resolver,omitempty"` diff --git a/protocol/hysteria2/inbound.go b/protocol/hysteria2/inbound.go index b706b492..cd7839bb 100644 --- a/protocol/hysteria2/inbound.go +++ b/protocol/hysteria2/inbound.go @@ -125,6 +125,15 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo } var realmOptions *realm.Options if options.Realm != nil { + if options.Realm.IPVersion != 0 && options.ListenOptions.Listen != nil { + listenAddr := netip.Addr(*options.ListenOptions.Listen).Unmap() + if options.Realm.IPVersion == 6 && listenAddr.Is4() { + return nil, E.New("realm.ip_version 6 conflicts with listen address ", listenAddr) + } + if options.Realm.IPVersion == 4 && listenAddr.Is6() && !listenAddr.IsUnspecified() { + return nil, E.New("realm.ip_version 4 conflicts with listen address ", listenAddr) + } + } queryOptions, err := adapter.DNSQueryOptionsFrom(ctx, options.Realm.STUNDomainResolver) if err != nil { return nil, err @@ -150,7 +159,14 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo } return dnsRouter.Lookup(ctx, host, dnsOptions) }, - Logger: logger, + Logger: logger, + IPVersion: options.Realm.IPVersion, + } + if options.Realm.PortMapping != nil && options.Realm.PortMapping.Enabled { + realmOptions.PortMapping = &realm.PortMappingOptions{ + Timeout: time.Duration(options.Realm.PortMapping.Timeout), + Lifetime: time.Duration(options.Realm.PortMapping.Lifetime), + } } } hysteriaService, err := hysteria2.NewService[int](hysteria2.ServiceOptions{ diff --git a/protocol/hysteria2/outbound.go b/protocol/hysteria2/outbound.go index 7651a0c6..742940ed 100644 --- a/protocol/hysteria2/outbound.go +++ b/protocol/hysteria2/outbound.go @@ -111,7 +111,14 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL } return dnsRouter.Lookup(ctx, host, dnsOptions) }, - Logger: logger, + Logger: logger, + IPVersion: options.Realm.IPVersion, + } + if options.Realm.PortMapping != nil && options.Realm.PortMapping.Enabled { + realmOptions.PortMapping = &realm.PortMappingOptions{ + Timeout: time.Duration(options.Realm.PortMapping.Timeout), + Lifetime: time.Duration(options.Realm.PortMapping.Lifetime), + } } } networkList := options.Network.Build()