feat: support range format for hysteria2 hop-interval

This commit is contained in:
wwqgtxx
2026-04-11 20:51:29 +08:00
parent d8cb32b856
commit 80072ebb6f
6 changed files with 84 additions and 61 deletions
+17 -9
View File
@@ -42,7 +42,7 @@ type Hysteria2Option struct {
Server string `proxy:"server"`
Port int `proxy:"port,omitempty"`
Ports string `proxy:"ports,omitempty"`
HopInterval int `proxy:"hop-interval,omitempty"`
HopInterval string `proxy:"hop-interval,omitempty"`
Up string `proxy:"up,omitempty"`
Down string `proxy:"down,omitempty"`
Password string `proxy:"password,omitempty"`
@@ -189,7 +189,7 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
ServerAddress: M.ParseSocksaddr(addr),
PacketListener: outbound.dialer,
QuicDialer: qtls.QuicDialerFunc(func(ctx context.Context, addr string, dialer qtls.PacketDialer, tlsCfg *tls.Config, cfg *quic.Config, early bool) (net.PacketConn, *quic.Conn, error) {
err = echConfig.ClientHandle(ctx, tlsCfg)
err := echConfig.ClientHandle(ctx, tlsCfg)
if err != nil {
return nil, nil, err
}
@@ -197,10 +197,9 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
}),
}
var ranges utils.IntRanges[uint16]
var serverPorts []uint16
if option.Ports != "" {
ranges, err = utils.NewUnsignedRanges[uint16](option.Ports)
ranges, err := utils.NewUnsignedRanges[uint16](option.Ports)
if err != nil {
return nil, err
}
@@ -209,12 +208,21 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
return true
})
if len(serverPorts) > 0 {
if option.HopInterval == 0 {
option.HopInterval = defaultHopInterval
} else if option.HopInterval < minHopInterval {
option.HopInterval = minHopInterval
hopRange, err := utils.NewUnsignedRange[uint64](option.HopInterval)
if err != nil {
return nil, err
}
clientOptions.HopInterval = time.Duration(option.HopInterval) * time.Second
start, end := hopRange.Start(), hopRange.End()
if start == 0 {
start = defaultHopInterval
} else if start < minHopInterval {
start = minHopInterval
}
if end < start {
end = start
}
clientOptions.HopInterval = time.Duration(start) * time.Second
clientOptions.HopIntervalMax = time.Duration(end) * time.Second
clientOptions.ServerPorts = serverPorts
}
}
+59
View File
@@ -1,6 +1,10 @@
package utils
import (
"fmt"
"strconv"
"strings"
"golang.org/x/exp/constraints"
)
@@ -42,3 +46,58 @@ func (r Range[T]) Start() T {
func (r Range[T]) End() T {
return r.end
}
func (r Range[T]) String() string {
if r.start == r.end {
return fmt.Sprintf("%v", r.start)
}
return fmt.Sprintf("%v-%v", r.start, r.end)
}
func NewUnsignedRange[T constraints.Unsigned](expected string) (Range[T], error) {
return newIntRange(expected, parseUnsigned[T])
}
func NewSignedRange[T constraints.Signed](expected string) (Range[T], error) {
return newIntRange(expected, parseSigned[T])
}
func newIntRange[T constraints.Integer](s string, parseFn func(string) (T, error)) (Range[T], error) {
s = strings.TrimSpace(s)
if len(s) == 0 {
return NewRange[T](0, 0), nil
}
status := strings.Split(s, "-")
start, err := parseFn(strings.Trim(status[0], "[ ]"))
if err != nil {
return Range[T]{}, fmt.Errorf("invalid range: %s", s)
}
switch len(status) {
case 1: // Port range
return NewRange(start, start), nil
case 2: // Single port
end, err := parseFn(strings.Trim(status[1], "[ ]"))
if err != nil {
return Range[T]{}, fmt.Errorf("invalid range: %s", s)
}
return NewRange(start, end), nil
default:
return Range[T]{}, fmt.Errorf("invalid range: %s", s)
}
}
func parseUnsigned[T constraints.Unsigned](s string) (T, error) {
if val, err := strconv.ParseUint(s, 10, 64); err == nil {
return T(val), nil
} else {
return 0, err
}
}
func parseSigned[T constraints.Signed](s string) (T, error) {
if val, err := strconv.ParseInt(s, 10, 64); err == nil {
return T(val), nil
} else {
return 0, err
}
}
+4 -48
View File
@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"sort"
"strconv"
"strings"
"golang.org/x/exp/constraints"
@@ -38,41 +37,16 @@ func newIntRangesFromList[T constraints.Integer](list []string, parseFn func(str
continue
}
status := strings.Split(s, "-")
statusLen := len(status)
if statusLen > 2 {
return nil, errIntRanges
}
start, err := parseFn(strings.Trim(status[0], "[ ]"))
r, err := newIntRange[T](s, parseFn)
if err != nil {
return nil, errIntRanges
}
switch statusLen {
case 1: // Port range
ranges = append(ranges, NewRange(T(start), T(start)))
case 2: // Single port
end, err := parseFn(strings.Trim(status[1], "[ ]"))
if err != nil {
return nil, errIntRanges
}
ranges = append(ranges, NewRange(T(start), T(end)))
return nil, err
}
ranges = append(ranges, r)
}
return ranges, nil
}
func parseUnsigned[T constraints.Unsigned](s string) (T, error) {
if val, err := strconv.ParseUint(s, 10, 64); err == nil {
return T(val), nil
} else {
return 0, err
}
}
func NewUnsignedRanges[T constraints.Unsigned](expected string) (IntRanges[T], error) {
return newIntRanges(expected, parseUnsigned[T])
}
@@ -81,14 +55,6 @@ func NewUnsignedRangesFromList[T constraints.Unsigned](list []string) (IntRanges
return newIntRangesFromList(list, parseUnsigned[T])
}
func parseSigned[T constraints.Signed](s string) (T, error) {
if val, err := strconv.ParseInt(s, 10, 64); err == nil {
return T(val), nil
} else {
return 0, err
}
}
func NewSignedRanges[T constraints.Signed](expected string) (IntRanges[T], error) {
return newIntRanges(expected, parseSigned[T])
}
@@ -118,17 +84,7 @@ func (ranges IntRanges[T]) String() string {
terms := make([]string, len(ranges))
for i, r := range ranges {
start := r.Start()
end := r.End()
var term string
if start == end {
term = strconv.Itoa(int(start))
} else {
term = strconv.Itoa(int(start)) + "-" + strconv.Itoa(int(end))
}
terms[i] = term
terms[i] = r.String()
}
return strings.Join(terms, "/")
+1 -1
View File
@@ -982,7 +982,7 @@ proxies: # socks5
server: server.com
port: 443
# ports: 1000,2000-3000,5000 # port 不可省略
# hop-interval: 15
# hop-interval: 15 # 支持填写"15-30"会每次随机选取其中一个值作为切换间隔,仅支持写一个范围(即不允许出现逗号)
# up 和 down 均不写或为 0 则使用 BBR 流控
# up: "30 Mbps" # 若不写单位,默认为 Mbps
# down: "200 Mbps" # 若不写单位,默认为 Mbps
+1 -1
View File
@@ -30,7 +30,7 @@ require (
github.com/metacubex/restls-client-go v0.1.7
github.com/metacubex/sing v0.5.7
github.com/metacubex/sing-mux v0.3.5
github.com/metacubex/sing-quic v0.0.0-20260411110907-47ab7e53e7c8
github.com/metacubex/sing-quic v0.0.0-20260411121336-043f0078bb1c
github.com/metacubex/sing-shadowsocks v0.2.12
github.com/metacubex/sing-shadowsocks2 v0.2.7
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2
+2 -2
View File
@@ -127,8 +127,8 @@ github.com/metacubex/sing v0.5.7 h1:8OC+fhKFSv/l9ehEhJRaZZAOuthfZo68SteBVLe8QqM=
github.com/metacubex/sing v0.5.7/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing-mux v0.3.5 h1:UqVN+o62SR8kJaC9/3VfOc5UiVqgVY/ef9WwfGYYkk0=
github.com/metacubex/sing-mux v0.3.5/go.mod h1:8bT7ZKT3clRrJjYc/x5CRYibC1TX/bK73a3r3+2E+Fc=
github.com/metacubex/sing-quic v0.0.0-20260411110907-47ab7e53e7c8 h1:yXGVlQowFsZi5TIcHVGRk2Ui7BPm6CDjKxakcFOYhPw=
github.com/metacubex/sing-quic v0.0.0-20260411110907-47ab7e53e7c8/go.mod h1:+lgKTd52xAarGtqugALISShyw4KxnoEpYe2u0zJh26w=
github.com/metacubex/sing-quic v0.0.0-20260411121336-043f0078bb1c h1:0WG/3NN/+67lDOXaZUPXtSyVq5jNz0xvsE3fiyVwUR0=
github.com/metacubex/sing-quic v0.0.0-20260411121336-043f0078bb1c/go.mod h1:+lgKTd52xAarGtqugALISShyw4KxnoEpYe2u0zJh26w=
github.com/metacubex/sing-shadowsocks v0.2.12 h1:Wqzo8bYXrK5aWqxu/TjlTnYZzAKtKsaFQBdr6IHFaBE=
github.com/metacubex/sing-shadowsocks v0.2.12/go.mod h1:2e5EIaw0rxKrm1YTRmiMnDulwbGxH9hAFlrwQLQMQkU=
github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6wVj3PPBVhor3A=