From 80072ebb6f9933e6a87ae94e3f6917a6c82c665e Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Sat, 11 Apr 2026 20:51:29 +0800 Subject: [PATCH] feat: support range format for hysteria2 `hop-interval` --- adapter/outbound/hysteria2.go | 26 +++++++++------ common/utils/range.go | 59 +++++++++++++++++++++++++++++++++++ common/utils/ranges.go | 52 +++--------------------------- docs/config.yaml | 2 +- go.mod | 2 +- go.sum | 4 +-- 6 files changed, 84 insertions(+), 61 deletions(-) diff --git a/adapter/outbound/hysteria2.go b/adapter/outbound/hysteria2.go index 4eceb1b9..e3ca1585 100644 --- a/adapter/outbound/hysteria2.go +++ b/adapter/outbound/hysteria2.go @@ -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 } } diff --git a/common/utils/range.go b/common/utils/range.go index 7b4a235c..486bc18d 100644 --- a/common/utils/range.go +++ b/common/utils/range.go @@ -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 + } +} diff --git a/common/utils/ranges.go b/common/utils/ranges.go index 21eacda5..1571e7ab 100644 --- a/common/utils/ranges.go +++ b/common/utils/ranges.go @@ -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, "/") diff --git a/docs/config.yaml b/docs/config.yaml index 5d8ad6fe..9f2aa6a9 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -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 diff --git a/go.mod b/go.mod index e5cc4c51..bb7aa9d1 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 5861ce64..8e9638ab 100644 --- a/go.sum +++ b/go.sum @@ -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=