mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-13 21:54:13 +08:00
Compare commits
13 Commits
dev-next
...
v1.5.0-bet
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6030e65605 | ||
![]() |
88d39f1c73 | ||
![]() |
189676578a | ||
![]() |
00609550c9 | ||
![]() |
80eadf48b4 | ||
![]() |
c237d1a692 | ||
![]() |
8cf7c05433 | ||
![]() |
9de7048e54 | ||
![]() |
ea30765dd6 | ||
![]() |
f2ef9530f1 | ||
![]() |
8feb11744e | ||
![]() |
6984a27a0d | ||
![]() |
6e9950b7b5 |
@ -16,6 +16,7 @@ builds:
|
|||||||
- with_quic
|
- with_quic
|
||||||
- with_dhcp
|
- with_dhcp
|
||||||
- with_wireguard
|
- with_wireguard
|
||||||
|
- with_ech
|
||||||
- with_utls
|
- with_utls
|
||||||
- with_reality_server
|
- with_reality_server
|
||||||
- with_clash_api
|
- with_clash_api
|
||||||
@ -51,6 +52,7 @@ builds:
|
|||||||
- with_quic
|
- with_quic
|
||||||
- with_dhcp
|
- with_dhcp
|
||||||
- with_wireguard
|
- with_wireguard
|
||||||
|
- with_ech
|
||||||
- with_utls
|
- with_utls
|
||||||
- with_clash_api
|
- with_clash_api
|
||||||
env:
|
env:
|
||||||
|
@ -9,7 +9,7 @@ RUN set -ex \
|
|||||||
&& apk add git build-base \
|
&& apk add git build-base \
|
||||||
&& export COMMIT=$(git rev-parse --short HEAD) \
|
&& export COMMIT=$(git rev-parse --short HEAD) \
|
||||||
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
||||||
&& go build -v -trimpath -tags with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_clash_api,with_acme \
|
&& go build -v -trimpath -tags with_gvisor,with_quic,with_dhcp,with_wireguard,with_ech,with_utls,with_reality_server,with_clash_api,with_acme \
|
||||||
-o /go/bin/sing-box \
|
-o /go/bin/sing-box \
|
||||||
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
||||||
./cmd/sing-box
|
./cmd/sing-box
|
||||||
|
2
Makefile
2
Makefile
@ -1,7 +1,7 @@
|
|||||||
NAME = sing-box
|
NAME = sing-box
|
||||||
COMMIT = $(shell git rev-parse --short HEAD)
|
COMMIT = $(shell git rev-parse --short HEAD)
|
||||||
TAGS_GO118 = with_gvisor,with_dhcp,with_wireguard,with_utls,with_reality_server,with_clash_api
|
TAGS_GO118 = with_gvisor,with_dhcp,with_wireguard,with_utls,with_reality_server,with_clash_api
|
||||||
TAGS_GO120 = with_quic
|
TAGS_GO120 = with_quic,with_ech
|
||||||
TAGS ?= $(TAGS_GO118),$(TAGS_GO120)
|
TAGS ?= $(TAGS_GO118),$(TAGS_GO120)
|
||||||
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server,with_shadowsocksr
|
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server,with_shadowsocksr
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/sagernet/sing-tun"
|
"github.com/sagernet/sing-tun"
|
||||||
"github.com/sagernet/sing/common/control"
|
"github.com/sagernet/sing/common/control"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
|
||||||
mdns "github.com/miekg/dns"
|
mdns "github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
@ -45,8 +46,6 @@ type Router interface {
|
|||||||
PackageManager() tun.PackageManager
|
PackageManager() tun.PackageManager
|
||||||
Rules() []Rule
|
Rules() []Rule
|
||||||
|
|
||||||
TimeService
|
|
||||||
|
|
||||||
ClashServer() ClashServer
|
ClashServer() ClashServer
|
||||||
SetClashServer(server ClashServer)
|
SetClashServer(server ClashServer)
|
||||||
|
|
||||||
@ -56,18 +55,12 @@ type Router interface {
|
|||||||
ResetNetwork() error
|
ResetNetwork() error
|
||||||
}
|
}
|
||||||
|
|
||||||
type routerContextKey struct{}
|
|
||||||
|
|
||||||
func ContextWithRouter(ctx context.Context, router Router) context.Context {
|
func ContextWithRouter(ctx context.Context, router Router) context.Context {
|
||||||
return context.WithValue(ctx, (*routerContextKey)(nil), router)
|
return service.ContextWith(ctx, router)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RouterFromContext(ctx context.Context) Router {
|
func RouterFromContext(ctx context.Context) Router {
|
||||||
metadata := ctx.Value((*routerContextKey)(nil))
|
return service.FromContext[Router](ctx)
|
||||||
if metadata == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return metadata.(Router)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Rule interface {
|
type Rule interface {
|
||||||
|
2
box.go
2
box.go
@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
F "github.com/sagernet/sing/common/format"
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
"github.com/sagernet/sing/service/pause"
|
"github.com/sagernet/sing/service/pause"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -47,6 +48,7 @@ func New(options Options) (*Box, error) {
|
|||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
|
ctx = service.ContextWithDefaultRegistry(ctx)
|
||||||
ctx = pause.ContextWithDefaultManager(ctx)
|
ctx = pause.ContextWithDefaultManager(ctx)
|
||||||
createdAt := time.Now()
|
createdAt := time.Now()
|
||||||
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
||||||
|
@ -54,7 +54,7 @@ func init() {
|
|||||||
sharedFlags = append(sharedFlags, "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
|
sharedFlags = append(sharedFlags, "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
|
||||||
debugFlags = append(debugFlags, "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
debugFlags = append(debugFlags, "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
||||||
|
|
||||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api")
|
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_ech", "with_utls", "with_clash_api")
|
||||||
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
|
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
|
||||||
debugTags = append(debugTags, "debug")
|
debugTags = append(debugTags, "debug")
|
||||||
}
|
}
|
||||||
|
@ -29,8 +29,7 @@ func main() {
|
|||||||
newContent, updated0 := findAndReplace(objectsMap, projectContent, []string{"io.nekohasekai.sfa"}, newVersion.VersionString())
|
newContent, updated0 := findAndReplace(objectsMap, projectContent, []string{"io.nekohasekai.sfa"}, newVersion.VersionString())
|
||||||
newContent, updated1 := findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfa.independent", "io.nekohasekai.sfa.system"}, newVersion.String())
|
newContent, updated1 := findAndReplace(objectsMap, newContent, []string{"io.nekohasekai.sfa.independent", "io.nekohasekai.sfa.system"}, newVersion.String())
|
||||||
if updated0 || updated1 {
|
if updated0 || updated1 {
|
||||||
log.Info("updated version to ", newVersion.VersionString())
|
log.Info("updated version to ", newVersion.VersionString(), " (", newVersion.String(), ")")
|
||||||
common.Must(os.WriteFile("sing-box.xcodeproj/project.pbxproj.bak", []byte(projectContent), 0o644))
|
|
||||||
common.Must(os.WriteFile("sing-box.xcodeproj/project.pbxproj", []byte(newContent), 0o644))
|
common.Must(os.WriteFile("sing-box.xcodeproj/project.pbxproj", []byte(newContent), 0o644))
|
||||||
} else {
|
} else {
|
||||||
log.Info("version not changed")
|
log.Info("version not changed")
|
||||||
|
39
cmd/sing-box/cmd_generate_ech.go
Normal file
39
cmd/sing-box/cmd_generate_ech.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pqSignatureSchemesEnabled bool
|
||||||
|
|
||||||
|
var commandGenerateECHKeyPair = &cobra.Command{
|
||||||
|
Use: "ech-keypair <plain_server_name>",
|
||||||
|
Short: "Generate TLS ECH key pair",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := generateECHKeyPair(args[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandGenerateECHKeyPair.Flags().BoolVar(&pqSignatureSchemesEnabled, "pq-signature-schemes-enabled", false, "Enable PQ signature schemes")
|
||||||
|
commandGenerate.AddCommand(commandGenerateECHKeyPair)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateECHKeyPair(serverName string) error {
|
||||||
|
configPem, keyPem, err := tls.ECHKeygenDefault(serverName, pqSignatureSchemesEnabled)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
os.Stdout.WriteString(configPem)
|
||||||
|
os.Stdout.WriteString(keyPem)
|
||||||
|
return nil
|
||||||
|
}
|
120
common/qtls/wrapper.go
Normal file
120
common/qtls/wrapper.go
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
package qtls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/sagernet/quic-go"
|
||||||
|
"github.com/sagernet/quic-go/http3"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
aTLS "github.com/sagernet/sing/common/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
type QUICConfig interface {
|
||||||
|
Dial(ctx context.Context, conn net.PacketConn, addr net.Addr, config *quic.Config) (quic.Connection, error)
|
||||||
|
DialEarly(ctx context.Context, conn net.PacketConn, addr net.Addr, config *quic.Config) (quic.EarlyConnection, error)
|
||||||
|
CreateTransport(conn net.PacketConn, quicConnPtr *quic.EarlyConnection, serverAddr M.Socksaddr, quicConfig *quic.Config, enableDatagrams bool) http.RoundTripper
|
||||||
|
}
|
||||||
|
|
||||||
|
type QUICServerConfig interface {
|
||||||
|
Listen(conn net.PacketConn, config *quic.Config) (QUICListener, error)
|
||||||
|
ListenEarly(conn net.PacketConn, config *quic.Config) (QUICEarlyListener, error)
|
||||||
|
ConfigureHTTP3()
|
||||||
|
}
|
||||||
|
|
||||||
|
type QUICListener interface {
|
||||||
|
Accept(ctx context.Context) (quic.Connection, error)
|
||||||
|
Close() error
|
||||||
|
Addr() net.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
type QUICEarlyListener interface {
|
||||||
|
Accept(ctx context.Context) (quic.EarlyConnection, error)
|
||||||
|
Close() error
|
||||||
|
Addr() net.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func Dial(ctx context.Context, conn net.PacketConn, addr net.Addr, config aTLS.Config, quicConfig *quic.Config) (quic.Connection, error) {
|
||||||
|
if quicTLSConfig, isQUICConfig := config.(QUICConfig); isQUICConfig {
|
||||||
|
return quicTLSConfig.Dial(ctx, conn, addr, quicConfig)
|
||||||
|
}
|
||||||
|
tlsConfig, err := config.Config()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return quic.Dial(ctx, conn, addr, tlsConfig, quicConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DialEarly(ctx context.Context, conn net.PacketConn, addr net.Addr, config aTLS.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) {
|
||||||
|
if quicTLSConfig, isQUICConfig := config.(QUICConfig); isQUICConfig {
|
||||||
|
return quicTLSConfig.DialEarly(ctx, conn, addr, quicConfig)
|
||||||
|
}
|
||||||
|
tlsConfig, err := config.Config()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return quic.DialEarly(ctx, conn, addr, tlsConfig, quicConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateTransport(conn net.PacketConn, quicConnPtr *quic.EarlyConnection, serverAddr M.Socksaddr, config aTLS.Config, quicConfig *quic.Config, enableDatagrams bool) (http.RoundTripper, error) {
|
||||||
|
if quicTLSConfig, isQUICConfig := config.(QUICConfig); isQUICConfig {
|
||||||
|
return quicTLSConfig.CreateTransport(conn, quicConnPtr, serverAddr, quicConfig, enableDatagrams), nil
|
||||||
|
}
|
||||||
|
tlsConfig, err := config.Config()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &http3.RoundTripper{
|
||||||
|
TLSClientConfig: tlsConfig,
|
||||||
|
QuicConfig: quicConfig,
|
||||||
|
EnableDatagrams: enableDatagrams,
|
||||||
|
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||||
|
quicConn, err := quic.DialEarly(ctx, conn, serverAddr.UDPAddr(), tlsCfg, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
*quicConnPtr = quicConn
|
||||||
|
return quicConn, nil
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Listen(conn net.PacketConn, config aTLS.ServerConfig, quicConfig *quic.Config) (QUICListener, error) {
|
||||||
|
if quicTLSConfig, isQUICConfig := config.(QUICServerConfig); isQUICConfig {
|
||||||
|
return quicTLSConfig.Listen(conn, quicConfig)
|
||||||
|
}
|
||||||
|
tlsConfig, err := config.Config()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return quic.Listen(conn, tlsConfig, quicConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListenEarly(conn net.PacketConn, config aTLS.ServerConfig, quicConfig *quic.Config) (QUICEarlyListener, error) {
|
||||||
|
if quicTLSConfig, isQUICConfig := config.(QUICServerConfig); isQUICConfig {
|
||||||
|
return quicTLSConfig.ListenEarly(conn, quicConfig)
|
||||||
|
}
|
||||||
|
tlsConfig, err := config.Config()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return quic.ListenEarly(conn, tlsConfig, quicConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfigureHTTP3(config aTLS.ServerConfig) error {
|
||||||
|
if len(config.NextProtos()) == 0 {
|
||||||
|
config.SetNextProtos([]string{http3.NextProtoH3})
|
||||||
|
}
|
||||||
|
if quicTLSConfig, isQUICConfig := config.(QUICServerConfig); isQUICConfig {
|
||||||
|
quicTLSConfig.ConfigureHTTP3()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tlsConfig, err := config.Config()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
http3.ConfigureTLSConfig(tlsConfig)
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,43 +1,73 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
F "github.com/sagernet/sing/common/format"
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
"github.com/sagernet/sing/common/shell"
|
"github.com/sagernet/sing/common/shell"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
type AndroidSystemProxy struct {
|
||||||
useRish bool
|
useRish bool
|
||||||
rishPath string
|
rishPath string
|
||||||
)
|
serverAddr M.Socksaddr
|
||||||
|
supportSOCKS bool
|
||||||
|
isEnabled bool
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*AndroidSystemProxy, error) {
|
||||||
userId := os.Getuid()
|
userId := os.Getuid()
|
||||||
|
var (
|
||||||
|
useRish bool
|
||||||
|
rishPath string
|
||||||
|
)
|
||||||
if userId == 0 || userId == 1000 || userId == 2000 {
|
if userId == 0 || userId == 1000 || userId == 2000 {
|
||||||
useRish = false
|
useRish = false
|
||||||
} else {
|
} else {
|
||||||
rishPath, useRish = C.FindPath("rish")
|
rishPath, useRish = C.FindPath("rish")
|
||||||
|
if !useRish {
|
||||||
|
return nil, E.Cause(os.ErrPermission, "root or system (adb) permission is required for set system proxy")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return &AndroidSystemProxy{
|
||||||
|
useRish: useRish,
|
||||||
func runAndroidShell(name string, args ...string) error {
|
rishPath: rishPath,
|
||||||
if !useRish {
|
serverAddr: serverAddr,
|
||||||
return shell.Exec(name, args...).Attach().Run()
|
supportSOCKS: supportSOCKS,
|
||||||
} else {
|
|
||||||
return shell.Exec("sh", rishPath, "-c", F.ToString(name, " ", strings.Join(args, " "))).Attach().Run()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetSystemProxy(router adapter.Router, port uint16, isMixed bool) (func() error, error) {
|
|
||||||
err := runAndroidShell("settings", "put", "global", "http_proxy", F.ToString("127.0.0.1:", port))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return func() error {
|
|
||||||
return runAndroidShell("settings", "put", "global", "http_proxy", ":0")
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *AndroidSystemProxy) IsEnabled() bool {
|
||||||
|
return p.isEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AndroidSystemProxy) Enable() error {
|
||||||
|
err := p.runAndroidShell("settings", "put", "global", "http_proxy", p.serverAddr.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.isEnabled = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AndroidSystemProxy) Disable() error {
|
||||||
|
err := p.runAndroidShell("settings", "put", "global", "http_proxy", ":0")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.isEnabled = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AndroidSystemProxy) runAndroidShell(name string, args ...string) error {
|
||||||
|
if !p.useRish {
|
||||||
|
return shell.Exec(name, args...).Attach().Run()
|
||||||
|
} else {
|
||||||
|
return shell.Exec("sh", p.rishPath, "-c", F.ToString(name, " ", strings.Join(args, " "))).Attach().Run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,56 +1,56 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-tun"
|
"github.com/sagernet/sing-tun"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
F "github.com/sagernet/sing/common/format"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
"github.com/sagernet/sing/common/shell"
|
"github.com/sagernet/sing/common/shell"
|
||||||
"github.com/sagernet/sing/common/x/list"
|
"github.com/sagernet/sing/common/x/list"
|
||||||
)
|
)
|
||||||
|
|
||||||
type systemProxy struct {
|
type DarwinSystemProxy struct {
|
||||||
monitor tun.DefaultInterfaceMonitor
|
monitor tun.DefaultInterfaceMonitor
|
||||||
interfaceName string
|
interfaceName string
|
||||||
element *list.Element[tun.DefaultInterfaceUpdateCallback]
|
element *list.Element[tun.DefaultInterfaceUpdateCallback]
|
||||||
port uint16
|
serverAddr M.Socksaddr
|
||||||
isMixed bool
|
supportSOCKS bool
|
||||||
|
isEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *systemProxy) update(event int) {
|
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*DarwinSystemProxy, error) {
|
||||||
newInterfaceName := p.monitor.DefaultInterfaceName(netip.IPv4Unspecified())
|
interfaceMonitor := adapter.RouterFromContext(ctx).InterfaceMonitor()
|
||||||
if p.interfaceName == newInterfaceName {
|
if interfaceMonitor == nil {
|
||||||
return
|
return nil, E.New("missing interface monitor")
|
||||||
}
|
}
|
||||||
if p.interfaceName != "" {
|
proxy := &DarwinSystemProxy{
|
||||||
_ = p.unset()
|
monitor: interfaceMonitor,
|
||||||
|
serverAddr: serverAddr,
|
||||||
|
supportSOCKS: supportSOCKS,
|
||||||
}
|
}
|
||||||
p.interfaceName = newInterfaceName
|
proxy.element = interfaceMonitor.RegisterCallback(proxy.update)
|
||||||
interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName)
|
return proxy, nil
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if p.isMixed {
|
|
||||||
err = shell.Exec("networksetup", "-setsocksfirewallproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run()
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
err = shell.Exec("networksetup", "-setwebproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run()
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
_ = shell.Exec("networksetup", "-setsecurewebproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *systemProxy) unset() error {
|
func (p *DarwinSystemProxy) IsEnabled() bool {
|
||||||
|
return p.isEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DarwinSystemProxy) Enable() error {
|
||||||
|
return p.update0()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DarwinSystemProxy) Disable() error {
|
||||||
interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName)
|
interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if p.isMixed {
|
if p.supportSOCKS {
|
||||||
err = shell.Exec("networksetup", "-setsocksfirewallproxystate", interfaceDisplayName, "off").Attach().Run()
|
err = shell.Exec("networksetup", "-setsocksfirewallproxystate", interfaceDisplayName, "off").Attach().Run()
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -59,9 +59,53 @@ func (p *systemProxy) unset() error {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
err = shell.Exec("networksetup", "-setsecurewebproxystate", interfaceDisplayName, "off").Attach().Run()
|
err = shell.Exec("networksetup", "-setsecurewebproxystate", interfaceDisplayName, "off").Attach().Run()
|
||||||
}
|
}
|
||||||
|
if err == nil {
|
||||||
|
p.isEnabled = false
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *DarwinSystemProxy) update(event int) {
|
||||||
|
if event&tun.EventInterfaceUpdate == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !p.isEnabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = p.update0()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DarwinSystemProxy) update0() error {
|
||||||
|
newInterfaceName := p.monitor.DefaultInterfaceName(netip.IPv4Unspecified())
|
||||||
|
if p.interfaceName == newInterfaceName {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if p.interfaceName != "" {
|
||||||
|
_ = p.Disable()
|
||||||
|
}
|
||||||
|
p.interfaceName = newInterfaceName
|
||||||
|
interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if p.supportSOCKS {
|
||||||
|
err = shell.Exec("networksetup", "-setsocksfirewallproxy", interfaceDisplayName, p.serverAddr.AddrString(), strconv.Itoa(int(p.serverAddr.Port))).Attach().Run()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = shell.Exec("networksetup", "-setwebproxy", interfaceDisplayName, p.serverAddr.AddrString(), strconv.Itoa(int(p.serverAddr.Port))).Attach().Run()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = shell.Exec("networksetup", "-setsecurewebproxy", interfaceDisplayName, p.serverAddr.AddrString(), strconv.Itoa(int(p.serverAddr.Port))).Attach().Run()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.isEnabled = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func getInterfaceDisplayName(name string) (string, error) {
|
func getInterfaceDisplayName(name string) (string, error) {
|
||||||
content, err := shell.Exec("networksetup", "-listallhardwareports").ReadOutput()
|
content, err := shell.Exec("networksetup", "-listallhardwareports").ReadOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -77,21 +121,3 @@ func getInterfaceDisplayName(name string) (string, error) {
|
|||||||
}
|
}
|
||||||
return "", E.New(name, " not found in networksetup -listallhardwareports")
|
return "", E.New(name, " not found in networksetup -listallhardwareports")
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetSystemProxy(router adapter.Router, port uint16, isMixed bool) (func() error, error) {
|
|
||||||
interfaceMonitor := router.InterfaceMonitor()
|
|
||||||
if interfaceMonitor == nil {
|
|
||||||
return nil, E.New("missing interface monitor")
|
|
||||||
}
|
|
||||||
proxy := &systemProxy{
|
|
||||||
monitor: interfaceMonitor,
|
|
||||||
port: port,
|
|
||||||
isMixed: isMixed,
|
|
||||||
}
|
|
||||||
proxy.update(tun.EventInterfaceUpdate)
|
|
||||||
proxy.element = interfaceMonitor.RegisterCallback(proxy.update)
|
|
||||||
return func() error {
|
|
||||||
interfaceMonitor.UnregisterCallback(proxy.element)
|
|
||||||
return proxy.unset()
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
@ -3,75 +3,161 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
F "github.com/sagernet/sing/common/format"
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
"github.com/sagernet/sing/common/shell"
|
"github.com/sagernet/sing/common/shell"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
type LinuxSystemProxy struct {
|
||||||
hasGSettings bool
|
hasGSettings bool
|
||||||
sudoUser string
|
hasKWriteConfig5 bool
|
||||||
)
|
sudoUser string
|
||||||
|
serverAddr M.Socksaddr
|
||||||
|
supportSOCKS bool
|
||||||
|
isEnabled bool
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*LinuxSystemProxy, error) {
|
||||||
hasGSettings = common.Error(exec.LookPath("gsettings")) == nil
|
hasGSettings := common.Error(exec.LookPath("gsettings")) == nil
|
||||||
|
hasKWriteConfig5 := common.Error(exec.LookPath("kwriteconfig5")) == nil
|
||||||
|
var sudoUser string
|
||||||
if os.Getuid() == 0 {
|
if os.Getuid() == 0 {
|
||||||
sudoUser = os.Getenv("SUDO_USER")
|
sudoUser = os.Getenv("SUDO_USER")
|
||||||
}
|
}
|
||||||
|
if !hasGSettings && !hasKWriteConfig5 {
|
||||||
|
return nil, E.New("unsupported desktop environment")
|
||||||
|
}
|
||||||
|
return &LinuxSystemProxy{
|
||||||
|
hasGSettings: hasGSettings,
|
||||||
|
hasKWriteConfig5: hasKWriteConfig5,
|
||||||
|
sudoUser: sudoUser,
|
||||||
|
serverAddr: serverAddr,
|
||||||
|
supportSOCKS: supportSOCKS,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runAsUser(name string, args ...string) error {
|
func (p *LinuxSystemProxy) IsEnabled() bool {
|
||||||
|
return p.isEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LinuxSystemProxy) Enable() error {
|
||||||
|
if p.hasGSettings {
|
||||||
|
err := p.runAsUser("gsettings", "set", "org.gnome.system.proxy.http", "enabled", "true")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if p.supportSOCKS {
|
||||||
|
err = p.setGnomeProxy("ftp", "http", "https", "socks")
|
||||||
|
} else {
|
||||||
|
err = p.setGnomeProxy("http", "https")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = p.runAsUser("gsettings", "set", "org.gnome.system.proxy", "use-same-proxy", F.ToString(p.supportSOCKS))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = p.runAsUser("gsettings", "set", "org.gnome.system.proxy", "mode", "manual")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.hasKWriteConfig5 {
|
||||||
|
err := p.runAsUser("kwriteconfig5", "--file", "kioslaverc", "--group", "'Proxy Settings'", "--key", "ProxyType", "1")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if p.supportSOCKS {
|
||||||
|
err = p.setKDEProxy("ftp", "http", "https", "socks")
|
||||||
|
} else {
|
||||||
|
err = p.setKDEProxy("http", "https")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = p.runAsUser("kwriteconfig5", "--file", "kioslaverc", "--group", "'Proxy Settings'", "--key", "Authmode", "0")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = p.runAsUser("dbus-send", "--type=signal", "/KIO/Scheduler", "org.kde.KIO.Scheduler.reparseSlaveConfiguration", "string:''")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.isEnabled = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LinuxSystemProxy) Disable() error {
|
||||||
|
if p.hasGSettings {
|
||||||
|
err := p.runAsUser("gsettings", "set", "org.gnome.system.proxy", "mode", "none")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.hasKWriteConfig5 {
|
||||||
|
err := p.runAsUser("kwriteconfig5", "--file", "kioslaverc", "--group", "'Proxy Settings'", "--key", "ProxyType", "0")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = p.runAsUser("dbus-send", "--type=signal", "/KIO/Scheduler", "org.kde.KIO.Scheduler.reparseSlaveConfiguration", "string:''")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.isEnabled = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LinuxSystemProxy) runAsUser(name string, args ...string) error {
|
||||||
if os.Getuid() != 0 {
|
if os.Getuid() != 0 {
|
||||||
return shell.Exec(name, args...).Attach().Run()
|
return shell.Exec(name, args...).Attach().Run()
|
||||||
} else if sudoUser != "" {
|
} else if p.sudoUser != "" {
|
||||||
return shell.Exec("su", "-", sudoUser, "-c", F.ToString(name, " ", strings.Join(args, " "))).Attach().Run()
|
return shell.Exec("su", "-", p.sudoUser, "-c", F.ToString(name, " ", strings.Join(args, " "))).Attach().Run()
|
||||||
} else {
|
} else {
|
||||||
return E.New("set system proxy: unable to set as root")
|
return E.New("set system proxy: unable to set as root")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetSystemProxy(router adapter.Router, port uint16, isMixed bool) (func() error, error) {
|
func (p *LinuxSystemProxy) setGnomeProxy(proxyTypes ...string) error {
|
||||||
if !hasGSettings {
|
|
||||||
return nil, E.New("unsupported desktop environment")
|
|
||||||
}
|
|
||||||
err := runAsUser("gsettings", "set", "org.gnome.system.proxy.http", "enabled", "true")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if isMixed {
|
|
||||||
err = setGnomeProxy(port, "ftp", "http", "https", "socks")
|
|
||||||
} else {
|
|
||||||
err = setGnomeProxy(port, "http", "https")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
err = runAsUser("gsettings", "set", "org.gnome.system.proxy", "use-same-proxy", F.ToString(isMixed))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
err = runAsUser("gsettings", "set", "org.gnome.system.proxy", "mode", "manual")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return func() error {
|
|
||||||
return runAsUser("gsettings", "set", "org.gnome.system.proxy", "mode", "none")
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setGnomeProxy(port uint16, proxyTypes ...string) error {
|
|
||||||
for _, proxyType := range proxyTypes {
|
for _, proxyType := range proxyTypes {
|
||||||
err := runAsUser("gsettings", "set", "org.gnome.system.proxy."+proxyType, "host", "127.0.0.1")
|
err := p.runAsUser("gsettings", "set", "org.gnome.system.proxy."+proxyType, "host", p.serverAddr.AddrString())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = runAsUser("gsettings", "set", "org.gnome.system.proxy."+proxyType, "port", F.ToString(port))
|
err = p.runAsUser("gsettings", "set", "org.gnome.system.proxy."+proxyType, "port", F.ToString(p.serverAddr.Port))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LinuxSystemProxy) setKDEProxy(proxyTypes ...string) error {
|
||||||
|
for _, proxyType := range proxyTypes {
|
||||||
|
var proxyUrl string
|
||||||
|
if proxyType == "socks" {
|
||||||
|
proxyUrl = "socks://" + p.serverAddr.String()
|
||||||
|
} else {
|
||||||
|
proxyUrl = "http://" + p.serverAddr.String()
|
||||||
|
}
|
||||||
|
err := p.runAsUser(
|
||||||
|
"kwriteconfig5",
|
||||||
|
"--file",
|
||||||
|
"kioslaverc",
|
||||||
|
"--group",
|
||||||
|
"Proxy Settings",
|
||||||
|
"--key", proxyType+"Proxy",
|
||||||
|
proxyUrl,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,12 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetSystemProxy(router adapter.Router, port uint16, isMixed bool) (func() error, error) {
|
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (SystemProxy, error) {
|
||||||
return nil, os.ErrInvalid
|
return nil, os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,43 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"context"
|
||||||
F "github.com/sagernet/sing/common/format"
|
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
"github.com/sagernet/sing/common/wininet"
|
"github.com/sagernet/sing/common/wininet"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetSystemProxy(router adapter.Router, port uint16, isMixed bool) (func() error, error) {
|
type WindowsSystemProxy struct {
|
||||||
err := wininet.SetSystemProxy(F.ToString("http://127.0.0.1:", port), "")
|
serverAddr M.Socksaddr
|
||||||
if err != nil {
|
supportSOCKS bool
|
||||||
return nil, err
|
isEnabled bool
|
||||||
}
|
}
|
||||||
return func() error {
|
|
||||||
return wininet.ClearSystemProxy()
|
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*WindowsSystemProxy, error) {
|
||||||
|
return &WindowsSystemProxy{
|
||||||
|
serverAddr: serverAddr,
|
||||||
|
supportSOCKS: supportSOCKS,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *WindowsSystemProxy) IsEnabled() bool {
|
||||||
|
return p.isEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *WindowsSystemProxy) Enable() error {
|
||||||
|
err := wininet.SetSystemProxy("http://"+p.serverAddr.String(), "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.isEnabled = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *WindowsSystemProxy) Disable() error {
|
||||||
|
err := wininet.ClearSystemProxy()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.isEnabled = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
7
common/settings/system_proxy.go
Normal file
7
common/settings/system_proxy.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
type SystemProxy interface {
|
||||||
|
IsEnabled() bool
|
||||||
|
Enable() error
|
||||||
|
Disable() error
|
||||||
|
}
|
@ -13,29 +13,29 @@ import (
|
|||||||
aTLS "github.com/sagernet/sing/common/tls"
|
aTLS "github.com/sagernet/sing/common/tls"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewDialerFromOptions(router adapter.Router, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
|
func NewDialerFromOptions(ctx context.Context, router adapter.Router, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
|
||||||
if !options.Enabled {
|
if !options.Enabled {
|
||||||
return dialer, nil
|
return dialer, nil
|
||||||
}
|
}
|
||||||
config, err := NewClient(router, serverAddress, options)
|
config, err := NewClient(ctx, serverAddress, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return NewDialer(dialer, config), nil
|
return NewDialer(dialer, config), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
func NewClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||||
if !options.Enabled {
|
if !options.Enabled {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if options.ECH != nil && options.ECH.Enabled {
|
if options.ECH != nil && options.ECH.Enabled {
|
||||||
return NewECHClient(router, serverAddress, options)
|
return NewECHClient(ctx, serverAddress, options)
|
||||||
} else if options.Reality != nil && options.Reality.Enabled {
|
} else if options.Reality != nil && options.Reality.Enabled {
|
||||||
return NewRealityClient(router, serverAddress, options)
|
return NewRealityClient(ctx, serverAddress, options)
|
||||||
} else if options.UTLS != nil && options.UTLS.Enabled {
|
} else if options.UTLS != nil && options.UTLS.Enabled {
|
||||||
return NewUTLSClient(router, serverAddress, options)
|
return NewUTLSClient(ctx, serverAddress, options)
|
||||||
} else {
|
} else {
|
||||||
return NewSTDClient(router, serverAddress, options)
|
return NewSTDClient(ctx, serverAddress, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,50 +7,53 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
cftls "github.com/sagernet/cloudflare-tls"
|
cftls "github.com/sagernet/cloudflare-tls"
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-dns"
|
"github.com/sagernet/sing-dns"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/ntp"
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
mDNS "github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ECHClientConfig struct {
|
type echClientConfig struct {
|
||||||
config *cftls.Config
|
config *cftls.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ECHClientConfig) ServerName() string {
|
func (c *echClientConfig) ServerName() string {
|
||||||
return e.config.ServerName
|
return c.config.ServerName
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ECHClientConfig) SetServerName(serverName string) {
|
func (c *echClientConfig) SetServerName(serverName string) {
|
||||||
e.config.ServerName = serverName
|
c.config.ServerName = serverName
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ECHClientConfig) NextProtos() []string {
|
func (c *echClientConfig) NextProtos() []string {
|
||||||
return e.config.NextProtos
|
return c.config.NextProtos
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ECHClientConfig) SetNextProtos(nextProto []string) {
|
func (c *echClientConfig) SetNextProtos(nextProto []string) {
|
||||||
e.config.NextProtos = nextProto
|
c.config.NextProtos = nextProto
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ECHClientConfig) Config() (*STDConfig, error) {
|
func (c *echClientConfig) Config() (*STDConfig, error) {
|
||||||
return nil, E.New("unsupported usage for ECH")
|
return nil, E.New("unsupported usage for ECH")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ECHClientConfig) Client(conn net.Conn) (Conn, error) {
|
func (c *echClientConfig) Client(conn net.Conn) (Conn, error) {
|
||||||
return &echConnWrapper{cftls.Client(conn, e.config)}, nil
|
return &echConnWrapper{cftls.Client(conn, c.config)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ECHClientConfig) Clone() Config {
|
func (c *echClientConfig) Clone() Config {
|
||||||
return &ECHClientConfig{
|
return &echClientConfig{
|
||||||
config: e.config.Clone(),
|
config: c.config.Clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +83,7 @@ func (c *echConnWrapper) Upstream() any {
|
|||||||
return c.Conn
|
return c.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewECHClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
func NewECHClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||||
var serverName string
|
var serverName string
|
||||||
if options.ServerName != "" {
|
if options.ServerName != "" {
|
||||||
serverName = options.ServerName
|
serverName = options.ServerName
|
||||||
@ -94,7 +97,7 @@ func NewECHClient(router adapter.Router, serverAddress string, options option.Ou
|
|||||||
}
|
}
|
||||||
|
|
||||||
var tlsConfig cftls.Config
|
var tlsConfig cftls.Config
|
||||||
tlsConfig.Time = router.TimeFunc()
|
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||||
if options.DisableSNI {
|
if options.DisableSNI {
|
||||||
tlsConfig.ServerName = "127.0.0.1"
|
tlsConfig.ServerName = "127.0.0.1"
|
||||||
} else {
|
} else {
|
||||||
@ -168,24 +171,36 @@ func NewECHClient(router adapter.Router, serverAddress string, options option.Ou
|
|||||||
tlsConfig.ECHEnabled = true
|
tlsConfig.ECHEnabled = true
|
||||||
tlsConfig.PQSignatureSchemesEnabled = options.ECH.PQSignatureSchemesEnabled
|
tlsConfig.PQSignatureSchemesEnabled = options.ECH.PQSignatureSchemesEnabled
|
||||||
tlsConfig.DynamicRecordSizingDisabled = options.ECH.DynamicRecordSizingDisabled
|
tlsConfig.DynamicRecordSizingDisabled = options.ECH.DynamicRecordSizingDisabled
|
||||||
if options.ECH.Config != "" {
|
|
||||||
clientConfigContent, err := base64.StdEncoding.DecodeString(options.ECH.Config)
|
var echConfig []byte
|
||||||
|
if len(options.ECH.Config) > 0 {
|
||||||
|
echConfig = []byte(strings.Join(options.ECH.Config, "\n"))
|
||||||
|
} else if options.ECH.ConfigPath != "" {
|
||||||
|
content, err := os.ReadFile(options.ECH.ConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, E.Cause(err, "read ECH config")
|
||||||
}
|
}
|
||||||
clientConfig, err := cftls.UnmarshalECHConfigs(clientConfigContent)
|
echConfig = content
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tlsConfig.ClientECHConfigs = clientConfig
|
|
||||||
} else {
|
|
||||||
tlsConfig.GetClientECHConfigs = fetchECHClientConfig(router)
|
|
||||||
}
|
}
|
||||||
return &ECHClientConfig{&tlsConfig}, nil
|
|
||||||
|
if len(echConfig) > 0 {
|
||||||
|
block, rest := pem.Decode(echConfig)
|
||||||
|
if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 {
|
||||||
|
return nil, E.New("invalid ECH configs pem")
|
||||||
|
}
|
||||||
|
echConfigs, err := cftls.UnmarshalECHConfigs(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "parse ECH configs")
|
||||||
|
}
|
||||||
|
tlsConfig.ClientECHConfigs = echConfigs
|
||||||
|
} else {
|
||||||
|
tlsConfig.GetClientECHConfigs = fetchECHClientConfig(ctx)
|
||||||
|
}
|
||||||
|
return &echClientConfig{&tlsConfig}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchECHClientConfig(router adapter.Router) func(ctx context.Context, serverName string) ([]cftls.ECHConfig, error) {
|
func fetchECHClientConfig(ctx context.Context) func(_ context.Context, serverName string) ([]cftls.ECHConfig, error) {
|
||||||
return func(ctx context.Context, serverName string) ([]cftls.ECHConfig, error) {
|
return func(_ context.Context, serverName string) ([]cftls.ECHConfig, error) {
|
||||||
message := &mDNS.Msg{
|
message := &mDNS.Msg{
|
||||||
MsgHdr: mDNS.MsgHdr{
|
MsgHdr: mDNS.MsgHdr{
|
||||||
RecursionDesired: true,
|
RecursionDesired: true,
|
||||||
@ -198,7 +213,7 @@ func fetchECHClientConfig(router adapter.Router) func(ctx context.Context, serve
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
response, err := router.Exchange(ctx, message)
|
response, err := adapter.RouterFromContext(ctx).Exchange(ctx, message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
169
common/tls/ech_keygen.go
Normal file
169
common/tls/ech_keygen.go
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
//go:build with_ech
|
||||||
|
|
||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/pem"
|
||||||
|
|
||||||
|
cftls "github.com/sagernet/cloudflare-tls"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
|
"github.com/cloudflare/circl/hpke"
|
||||||
|
"github.com/cloudflare/circl/kem"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ECHKeygenDefault(serverName string, pqSignatureSchemesEnabled bool) (configPem string, keyPem string, err error) {
|
||||||
|
cipherSuites := []echCipherSuite{
|
||||||
|
{
|
||||||
|
kdf: hpke.KDF_HKDF_SHA256,
|
||||||
|
aead: hpke.AEAD_AES128GCM,
|
||||||
|
}, {
|
||||||
|
kdf: hpke.KDF_HKDF_SHA256,
|
||||||
|
aead: hpke.AEAD_ChaCha20Poly1305,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
keyConfig := []myECHKeyConfig{
|
||||||
|
{id: 0, kem: hpke.KEM_X25519_HKDF_SHA256},
|
||||||
|
}
|
||||||
|
if pqSignatureSchemesEnabled {
|
||||||
|
keyConfig = append(keyConfig, myECHKeyConfig{id: 1, kem: hpke.KEM_X25519_KYBER768_DRAFT00})
|
||||||
|
}
|
||||||
|
|
||||||
|
keyPairs, err := echKeygen(0xfe0d, serverName, keyConfig, cipherSuites)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var configBuffer bytes.Buffer
|
||||||
|
var totalLen uint16
|
||||||
|
for _, keyPair := range keyPairs {
|
||||||
|
totalLen += uint16(len(keyPair.rawConf))
|
||||||
|
}
|
||||||
|
binary.Write(&configBuffer, binary.BigEndian, totalLen)
|
||||||
|
for _, keyPair := range keyPairs {
|
||||||
|
configBuffer.Write(keyPair.rawConf)
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyBuffer bytes.Buffer
|
||||||
|
for _, keyPair := range keyPairs {
|
||||||
|
keyBuffer.Write(keyPair.rawKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
configPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBuffer.Bytes()}))
|
||||||
|
keyPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBuffer.Bytes()}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type echKeyConfigPair struct {
|
||||||
|
id uint8
|
||||||
|
key cftls.EXP_ECHKey
|
||||||
|
rawKey []byte
|
||||||
|
conf myECHKeyConfig
|
||||||
|
rawConf []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type echCipherSuite struct {
|
||||||
|
kdf hpke.KDF
|
||||||
|
aead hpke.AEAD
|
||||||
|
}
|
||||||
|
|
||||||
|
type myECHKeyConfig struct {
|
||||||
|
id uint8
|
||||||
|
kem hpke.KEM
|
||||||
|
seed []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func echKeygen(version uint16, serverName string, conf []myECHKeyConfig, suite []echCipherSuite) ([]echKeyConfigPair, error) {
|
||||||
|
be := binary.BigEndian
|
||||||
|
// prepare for future update
|
||||||
|
if version != 0xfe0d {
|
||||||
|
return nil, E.New("unsupported ECH version", version)
|
||||||
|
}
|
||||||
|
|
||||||
|
suiteBuf := make([]byte, 0, len(suite)*4+2)
|
||||||
|
suiteBuf = be.AppendUint16(suiteBuf, uint16(len(suite))*4)
|
||||||
|
for _, s := range suite {
|
||||||
|
if !s.kdf.IsValid() || !s.aead.IsValid() {
|
||||||
|
return nil, E.New("invalid HPKE cipher suite")
|
||||||
|
}
|
||||||
|
suiteBuf = be.AppendUint16(suiteBuf, uint16(s.kdf))
|
||||||
|
suiteBuf = be.AppendUint16(suiteBuf, uint16(s.aead))
|
||||||
|
}
|
||||||
|
|
||||||
|
pairs := []echKeyConfigPair{}
|
||||||
|
for _, c := range conf {
|
||||||
|
pair := echKeyConfigPair{}
|
||||||
|
pair.id = c.id
|
||||||
|
pair.conf = c
|
||||||
|
|
||||||
|
if !c.kem.IsValid() {
|
||||||
|
return nil, E.New("invalid HPKE KEM")
|
||||||
|
}
|
||||||
|
|
||||||
|
kpGenerator := c.kem.Scheme().GenerateKeyPair
|
||||||
|
if len(c.seed) > 0 {
|
||||||
|
kpGenerator = func() (kem.PublicKey, kem.PrivateKey, error) {
|
||||||
|
pub, sec := c.kem.Scheme().DeriveKeyPair(c.seed)
|
||||||
|
return pub, sec, nil
|
||||||
|
}
|
||||||
|
if len(c.seed) < c.kem.Scheme().PrivateKeySize() {
|
||||||
|
return nil, E.New("HPKE KEM seed too short")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub, sec, err := kpGenerator()
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "generate ECH config key pair")
|
||||||
|
}
|
||||||
|
b := []byte{}
|
||||||
|
b = be.AppendUint16(b, version)
|
||||||
|
b = be.AppendUint16(b, 0) // length field
|
||||||
|
// contents
|
||||||
|
// key config
|
||||||
|
b = append(b, c.id)
|
||||||
|
b = be.AppendUint16(b, uint16(c.kem))
|
||||||
|
pubBuf, err := pub.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "serialize ECH public key")
|
||||||
|
}
|
||||||
|
b = be.AppendUint16(b, uint16(len(pubBuf)))
|
||||||
|
b = append(b, pubBuf...)
|
||||||
|
|
||||||
|
b = append(b, suiteBuf...)
|
||||||
|
// end key config
|
||||||
|
// max name len, not supported
|
||||||
|
b = append(b, 0)
|
||||||
|
// server name
|
||||||
|
b = append(b, byte(len(serverName)))
|
||||||
|
b = append(b, []byte(serverName)...)
|
||||||
|
// extensions, not supported
|
||||||
|
b = be.AppendUint16(b, 0)
|
||||||
|
|
||||||
|
be.PutUint16(b[2:], uint16(len(b)-4))
|
||||||
|
|
||||||
|
pair.rawConf = b
|
||||||
|
|
||||||
|
secBuf, err := sec.MarshalBinary()
|
||||||
|
sk := []byte{}
|
||||||
|
sk = be.AppendUint16(sk, uint16(len(secBuf)))
|
||||||
|
sk = append(sk, secBuf...)
|
||||||
|
sk = be.AppendUint16(sk, uint16(len(b)))
|
||||||
|
sk = append(sk, b...)
|
||||||
|
|
||||||
|
cfECHKeys, err := cftls.EXP_UnmarshalECHKeys(sk)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "bug: can't parse generated ECH server key")
|
||||||
|
}
|
||||||
|
if len(cfECHKeys) != 1 {
|
||||||
|
return nil, E.New("bug: unexpected server key count")
|
||||||
|
}
|
||||||
|
pair.key = cfECHKeys[0]
|
||||||
|
pair.rawKey = sk
|
||||||
|
|
||||||
|
pairs = append(pairs, pair)
|
||||||
|
}
|
||||||
|
return pairs, nil
|
||||||
|
}
|
56
common/tls/ech_quic.go
Normal file
56
common/tls/ech_quic.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
//go:build with_quic && with_ech
|
||||||
|
|
||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/sagernet/cloudflare-tls"
|
||||||
|
"github.com/sagernet/quic-go/ech"
|
||||||
|
"github.com/sagernet/quic-go/http3_ech"
|
||||||
|
"github.com/sagernet/sing-box/common/qtls"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ qtls.QUICConfig = (*echClientConfig)(nil)
|
||||||
|
_ qtls.QUICServerConfig = (*echServerConfig)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *echClientConfig) Dial(ctx context.Context, conn net.PacketConn, addr net.Addr, config *quic.Config) (quic.Connection, error) {
|
||||||
|
return quic.Dial(ctx, conn, addr, c.config, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *echClientConfig) DialEarly(ctx context.Context, conn net.PacketConn, addr net.Addr, config *quic.Config) (quic.EarlyConnection, error) {
|
||||||
|
return quic.DialEarly(ctx, conn, addr, c.config, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *echClientConfig) CreateTransport(conn net.PacketConn, quicConnPtr *quic.EarlyConnection, serverAddr M.Socksaddr, quicConfig *quic.Config, enableDatagrams bool) http.RoundTripper {
|
||||||
|
return &http3.RoundTripper{
|
||||||
|
TLSClientConfig: c.config,
|
||||||
|
QuicConfig: quicConfig,
|
||||||
|
EnableDatagrams: enableDatagrams,
|
||||||
|
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||||
|
quicConn, err := quic.DialEarly(ctx, conn, serverAddr.UDPAddr(), tlsCfg, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
*quicConnPtr = quicConn
|
||||||
|
return quicConn, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *echServerConfig) Listen(conn net.PacketConn, config *quic.Config) (qtls.QUICListener, error) {
|
||||||
|
return quic.Listen(conn, c.config, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *echServerConfig) ListenEarly(conn net.PacketConn, config *quic.Config) (qtls.QUICEarlyListener, error) {
|
||||||
|
return quic.ListenEarly(conn, c.config, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *echServerConfig) ConfigureHTTP3() {
|
||||||
|
http3.ConfigureTLSConfig(c.config)
|
||||||
|
}
|
343
common/tls/ech_server.go
Normal file
343
common/tls/ech_server.go
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
//go:build with_ech
|
||||||
|
|
||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/pem"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
cftls "github.com/sagernet/cloudflare-tls"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/ntp"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
)
|
||||||
|
|
||||||
|
type echServerConfig struct {
|
||||||
|
config *cftls.Config
|
||||||
|
logger log.Logger
|
||||||
|
certificate []byte
|
||||||
|
key []byte
|
||||||
|
certificatePath string
|
||||||
|
keyPath string
|
||||||
|
watcher *fsnotify.Watcher
|
||||||
|
echKeyPath string
|
||||||
|
echWatcher *fsnotify.Watcher
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *echServerConfig) ServerName() string {
|
||||||
|
return c.config.ServerName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *echServerConfig) SetServerName(serverName string) {
|
||||||
|
c.config.ServerName = serverName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *echServerConfig) NextProtos() []string {
|
||||||
|
return c.config.NextProtos
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *echServerConfig) SetNextProtos(nextProto []string) {
|
||||||
|
c.config.NextProtos = nextProto
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *echServerConfig) Config() (*STDConfig, error) {
|
||||||
|
return nil, E.New("unsupported usage for ECH")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *echServerConfig) Client(conn net.Conn) (Conn, error) {
|
||||||
|
return &echConnWrapper{cftls.Client(conn, c.config)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *echServerConfig) Server(conn net.Conn) (Conn, error) {
|
||||||
|
return &echConnWrapper{cftls.Server(conn, c.config)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *echServerConfig) Clone() Config {
|
||||||
|
return &echServerConfig{
|
||||||
|
config: c.config.Clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *echServerConfig) Start() error {
|
||||||
|
if c.certificatePath != "" && c.keyPath != "" {
|
||||||
|
err := c.startWatcher()
|
||||||
|
if err != nil {
|
||||||
|
c.logger.Warn("create fsnotify watcher: ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.echKeyPath != "" {
|
||||||
|
err := c.startECHWatcher()
|
||||||
|
if err != nil {
|
||||||
|
c.logger.Warn("create fsnotify watcher: ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *echServerConfig) startWatcher() error {
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if c.certificatePath != "" {
|
||||||
|
err = watcher.Add(c.certificatePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.keyPath != "" {
|
||||||
|
err = watcher.Add(c.keyPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.watcher = watcher
|
||||||
|
go c.loopUpdate()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *echServerConfig) loopUpdate() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-c.watcher.Events:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if event.Op&fsnotify.Write != fsnotify.Write {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err := c.reloadKeyPair()
|
||||||
|
if err != nil {
|
||||||
|
c.logger.Error(E.Cause(err, "reload TLS key pair"))
|
||||||
|
}
|
||||||
|
case err, ok := <-c.watcher.Errors:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.logger.Error(E.Cause(err, "fsnotify error"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *echServerConfig) reloadKeyPair() error {
|
||||||
|
if c.certificatePath != "" {
|
||||||
|
certificate, err := os.ReadFile(c.certificatePath)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "reload certificate from ", c.certificatePath)
|
||||||
|
}
|
||||||
|
c.certificate = certificate
|
||||||
|
}
|
||||||
|
if c.keyPath != "" {
|
||||||
|
key, err := os.ReadFile(c.keyPath)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "reload key from ", c.keyPath)
|
||||||
|
}
|
||||||
|
c.key = key
|
||||||
|
}
|
||||||
|
keyPair, err := cftls.X509KeyPair(c.certificate, c.key)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "reload key pair")
|
||||||
|
}
|
||||||
|
c.config.Certificates = []cftls.Certificate{keyPair}
|
||||||
|
c.logger.Info("reloaded TLS certificate")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *echServerConfig) startECHWatcher() error {
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = watcher.Add(c.echKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.echWatcher = watcher
|
||||||
|
go c.loopECHUpdate()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *echServerConfig) loopECHUpdate() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-c.echWatcher.Events:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if event.Op&fsnotify.Write != fsnotify.Write {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err := c.reloadECHKey()
|
||||||
|
if err != nil {
|
||||||
|
c.logger.Error(E.Cause(err, "reload ECH key"))
|
||||||
|
}
|
||||||
|
case err, ok := <-c.echWatcher.Errors:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.logger.Error(E.Cause(err, "fsnotify error"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *echServerConfig) reloadECHKey() error {
|
||||||
|
echKeyContent, err := os.ReadFile(c.echKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
block, rest := pem.Decode(echKeyContent)
|
||||||
|
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
|
||||||
|
return E.New("invalid ECH keys pem")
|
||||||
|
}
|
||||||
|
echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "parse ECH keys")
|
||||||
|
}
|
||||||
|
echKeySet, err := cftls.EXP_NewECHKeySet(echKeys)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "create ECH key set")
|
||||||
|
}
|
||||||
|
c.config.ServerECHProvider = echKeySet
|
||||||
|
c.logger.Info("reloaded ECH keys")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *echServerConfig) Close() error {
|
||||||
|
var err error
|
||||||
|
if c.watcher != nil {
|
||||||
|
err = E.Append(err, c.watcher.Close(), func(err error) error {
|
||||||
|
return E.Cause(err, "close certificate watcher")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if c.echWatcher != nil {
|
||||||
|
err = E.Append(err, c.echWatcher.Close(), func(err error) error {
|
||||||
|
return E.Cause(err, "close ECH key watcher")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewECHServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||||
|
if !options.Enabled {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var tlsConfig cftls.Config
|
||||||
|
if options.ACME != nil && len(options.ACME.Domain) > 0 {
|
||||||
|
return nil, E.New("acme is unavailable in ech")
|
||||||
|
}
|
||||||
|
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||||
|
if options.ServerName != "" {
|
||||||
|
tlsConfig.ServerName = options.ServerName
|
||||||
|
}
|
||||||
|
if len(options.ALPN) > 0 {
|
||||||
|
tlsConfig.NextProtos = append(options.ALPN, tlsConfig.NextProtos...)
|
||||||
|
}
|
||||||
|
if options.MinVersion != "" {
|
||||||
|
minVersion, err := ParseTLSVersion(options.MinVersion)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "parse min_version")
|
||||||
|
}
|
||||||
|
tlsConfig.MinVersion = minVersion
|
||||||
|
}
|
||||||
|
if options.MaxVersion != "" {
|
||||||
|
maxVersion, err := ParseTLSVersion(options.MaxVersion)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "parse max_version")
|
||||||
|
}
|
||||||
|
tlsConfig.MaxVersion = maxVersion
|
||||||
|
}
|
||||||
|
if options.CipherSuites != nil {
|
||||||
|
find:
|
||||||
|
for _, cipherSuite := range options.CipherSuites {
|
||||||
|
for _, tlsCipherSuite := range tls.CipherSuites() {
|
||||||
|
if cipherSuite == tlsCipherSuite.Name {
|
||||||
|
tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID)
|
||||||
|
continue find
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, E.New("unknown cipher_suite: ", cipherSuite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var certificate []byte
|
||||||
|
var key []byte
|
||||||
|
if len(options.Certificate) > 0 {
|
||||||
|
certificate = []byte(strings.Join(options.Certificate, "\n"))
|
||||||
|
} else if options.CertificatePath != "" {
|
||||||
|
content, err := os.ReadFile(options.CertificatePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "read certificate")
|
||||||
|
}
|
||||||
|
certificate = content
|
||||||
|
}
|
||||||
|
if len(options.Key) > 0 {
|
||||||
|
key = []byte(strings.Join(options.Key, "\n"))
|
||||||
|
} else if options.KeyPath != "" {
|
||||||
|
content, err := os.ReadFile(options.KeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "read key")
|
||||||
|
}
|
||||||
|
key = content
|
||||||
|
}
|
||||||
|
|
||||||
|
if certificate == nil {
|
||||||
|
return nil, E.New("missing certificate")
|
||||||
|
} else if key == nil {
|
||||||
|
return nil, E.New("missing key")
|
||||||
|
}
|
||||||
|
|
||||||
|
keyPair, err := cftls.X509KeyPair(certificate, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "parse x509 key pair")
|
||||||
|
}
|
||||||
|
tlsConfig.Certificates = []cftls.Certificate{keyPair}
|
||||||
|
|
||||||
|
var echKey []byte
|
||||||
|
if len(options.ECH.Key) > 0 {
|
||||||
|
echKey = []byte(strings.Join(options.ECH.Key, "\n"))
|
||||||
|
} else if options.KeyPath != "" {
|
||||||
|
content, err := os.ReadFile(options.ECH.KeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "read ECH key")
|
||||||
|
}
|
||||||
|
echKey = content
|
||||||
|
} else {
|
||||||
|
return nil, E.New("missing ECH key")
|
||||||
|
}
|
||||||
|
|
||||||
|
block, rest := pem.Decode(echKey)
|
||||||
|
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
|
||||||
|
return nil, E.New("invalid ECH keys pem")
|
||||||
|
}
|
||||||
|
|
||||||
|
echKeys, err := cftls.EXP_UnmarshalECHKeys(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "parse ECH keys")
|
||||||
|
}
|
||||||
|
|
||||||
|
echKeySet, err := cftls.EXP_NewECHKeySet(echKeys)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "create ECH key set")
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConfig.ECHEnabled = true
|
||||||
|
tlsConfig.PQSignatureSchemesEnabled = options.ECH.PQSignatureSchemesEnabled
|
||||||
|
tlsConfig.DynamicRecordSizingDisabled = options.ECH.DynamicRecordSizingDisabled
|
||||||
|
tlsConfig.ServerECHProvider = echKeySet
|
||||||
|
|
||||||
|
return &echServerConfig{
|
||||||
|
config: &tlsConfig,
|
||||||
|
logger: logger,
|
||||||
|
certificate: certificate,
|
||||||
|
key: key,
|
||||||
|
certificatePath: options.CertificatePath,
|
||||||
|
keyPath: options.KeyPath,
|
||||||
|
echKeyPath: options.ECH.KeyPath,
|
||||||
|
}, nil
|
||||||
|
}
|
@ -3,11 +3,23 @@
|
|||||||
package tls
|
package tls
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"context"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewECHClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
var errECHNotIncluded = E.New(`ECH is not included in this build, rebuild with -tags with_ech`)
|
||||||
return nil, E.New(`ECH is not included in this build, rebuild with -tags with_ech`)
|
|
||||||
|
func NewECHServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||||
|
return nil, errECHNotIncluded
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewECHClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||||
|
return nil, errECHNotIncluded
|
||||||
|
}
|
||||||
|
|
||||||
|
func ECHKeygenDefault(host string, pqSignatureSchemesEnabled bool) (configPem string, keyPem string, err error) {
|
||||||
|
return "", "", errECHNotIncluded
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing/common/debug"
|
"github.com/sagernet/sing/common/debug"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
@ -45,12 +44,12 @@ type RealityClientConfig struct {
|
|||||||
shortID [8]byte
|
shortID [8]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRealityClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (*RealityClientConfig, error) {
|
func NewRealityClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (*RealityClientConfig, error) {
|
||||||
if options.UTLS == nil || !options.UTLS.Enabled {
|
if options.UTLS == nil || !options.UTLS.Enabled {
|
||||||
return nil, E.New("uTLS is required by reality client")
|
return nil, E.New("uTLS is required by reality client")
|
||||||
}
|
}
|
||||||
|
|
||||||
uClient, err := NewUTLSClient(router, serverAddress, options)
|
uClient, err := NewUTLSClient(ctx, serverAddress, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ import (
|
|||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/common/ntp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ ServerConfigCompat = (*RealityServerConfig)(nil)
|
var _ ServerConfigCompat = (*RealityServerConfig)(nil)
|
||||||
@ -27,13 +28,13 @@ type RealityServerConfig struct {
|
|||||||
config *reality.Config
|
config *reality.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRealityServer(ctx context.Context, router adapter.Router, logger log.Logger, options option.InboundTLSOptions) (*RealityServerConfig, error) {
|
func NewRealityServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (*RealityServerConfig, error) {
|
||||||
var tlsConfig reality.Config
|
var tlsConfig reality.Config
|
||||||
|
|
||||||
if options.ACME != nil && len(options.ACME.Domain) > 0 {
|
if options.ACME != nil && len(options.ACME.Domain) > 0 {
|
||||||
return nil, E.New("acme is unavailable in reality")
|
return nil, E.New("acme is unavailable in reality")
|
||||||
}
|
}
|
||||||
tlsConfig.Time = router.TimeFunc()
|
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||||
if options.ServerName != "" {
|
if options.ServerName != "" {
|
||||||
tlsConfig.ServerName = options.ServerName
|
tlsConfig.ServerName = options.ServerName
|
||||||
}
|
}
|
||||||
@ -66,10 +67,10 @@ func NewRealityServer(ctx context.Context, router adapter.Router, logger log.Log
|
|||||||
return nil, E.New("unknown cipher_suite: ", cipherSuite)
|
return nil, E.New("unknown cipher_suite: ", cipherSuite)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if options.Certificate != "" || options.CertificatePath != "" {
|
if len(options.Certificate) > 0 || options.CertificatePath != "" {
|
||||||
return nil, E.New("certificate is unavailable in reality")
|
return nil, E.New("certificate is unavailable in reality")
|
||||||
}
|
}
|
||||||
if options.Key != "" || options.KeyPath != "" {
|
if len(options.Key) > 0 || options.KeyPath != "" {
|
||||||
return nil, E.New("key is unavailable in reality")
|
return nil, E.New("key is unavailable in reality")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,7 +102,7 @@ func NewRealityServer(ctx context.Context, router adapter.Router, logger log.Log
|
|||||||
tlsConfig.ShortIds[shortID] = true
|
tlsConfig.ShortIds[shortID] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
handshakeDialer, err := dialer.New(router, options.Reality.Handshake.DialerOptions)
|
handshakeDialer, err := dialer.New(adapter.RouterFromContext(ctx), options.Reality.Handshake.DialerOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,11 @@ package tls
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewRealityServer(ctx context.Context, router adapter.Router, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
|
func NewRealityServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||||
return nil, E.New(`reality server is not included in this build, rebuild with -tags with_reality_server`)
|
return nil, E.New(`reality server is not included in this build, rebuild with -tags with_reality_server`)
|
||||||
}
|
}
|
||||||
|
@ -4,21 +4,22 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
aTLS "github.com/sagernet/sing/common/tls"
|
aTLS "github.com/sagernet/sing/common/tls"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewServer(ctx context.Context, router adapter.Router, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
|
func NewServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||||
if !options.Enabled {
|
if !options.Enabled {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if options.Reality != nil && options.Reality.Enabled {
|
if options.ECH != nil && options.ECH.Enabled {
|
||||||
return NewRealityServer(ctx, router, logger, options)
|
return NewECHServer(ctx, logger, options)
|
||||||
|
} else if options.Reality != nil && options.Reality.Enabled {
|
||||||
|
return NewRealityServer(ctx, logger, options)
|
||||||
} else {
|
} else {
|
||||||
return NewSTDServer(ctx, router, logger, options)
|
return NewSTDServer(ctx, logger, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
package tls
|
package tls
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/ntp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type STDClientConfig struct {
|
type STDClientConfig struct {
|
||||||
@ -44,7 +45,7 @@ func (s *STDClientConfig) Clone() Config {
|
|||||||
return &STDClientConfig{s.config.Clone()}
|
return &STDClientConfig{s.config.Clone()}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSTDClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
func NewSTDClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||||
var serverName string
|
var serverName string
|
||||||
if options.ServerName != "" {
|
if options.ServerName != "" {
|
||||||
serverName = options.ServerName
|
serverName = options.ServerName
|
||||||
@ -58,7 +59,7 @@ func NewSTDClient(router adapter.Router, serverAddress string, options option.Ou
|
|||||||
}
|
}
|
||||||
|
|
||||||
var tlsConfig tls.Config
|
var tlsConfig tls.Config
|
||||||
tlsConfig.Time = router.TimeFunc()
|
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||||
if options.DisableSNI {
|
if options.DisableSNI {
|
||||||
tlsConfig.ServerName = "127.0.0.1"
|
tlsConfig.ServerName = "127.0.0.1"
|
||||||
} else {
|
} else {
|
||||||
|
@ -5,12 +5,14 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/ntp"
|
||||||
|
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
)
|
)
|
||||||
@ -156,7 +158,7 @@ func (c *STDServerConfig) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSTDServer(ctx context.Context, router adapter.Router, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
|
func NewSTDServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||||
if !options.Enabled {
|
if !options.Enabled {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@ -175,7 +177,7 @@ func NewSTDServer(ctx context.Context, router adapter.Router, logger log.Logger,
|
|||||||
} else {
|
} else {
|
||||||
tlsConfig = &tls.Config{}
|
tlsConfig = &tls.Config{}
|
||||||
}
|
}
|
||||||
tlsConfig.Time = router.TimeFunc()
|
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||||
if options.ServerName != "" {
|
if options.ServerName != "" {
|
||||||
tlsConfig.ServerName = options.ServerName
|
tlsConfig.ServerName = options.ServerName
|
||||||
}
|
}
|
||||||
@ -211,8 +213,8 @@ func NewSTDServer(ctx context.Context, router adapter.Router, logger log.Logger,
|
|||||||
var certificate []byte
|
var certificate []byte
|
||||||
var key []byte
|
var key []byte
|
||||||
if acmeService == nil {
|
if acmeService == nil {
|
||||||
if options.Certificate != "" {
|
if len(options.Certificate) > 0 {
|
||||||
certificate = []byte(options.Certificate)
|
certificate = []byte(strings.Join(options.Certificate, "\n"))
|
||||||
} else if options.CertificatePath != "" {
|
} else if options.CertificatePath != "" {
|
||||||
content, err := os.ReadFile(options.CertificatePath)
|
content, err := os.ReadFile(options.CertificatePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -220,8 +222,8 @@ func NewSTDServer(ctx context.Context, router adapter.Router, logger log.Logger,
|
|||||||
}
|
}
|
||||||
certificate = content
|
certificate = content
|
||||||
}
|
}
|
||||||
if options.Key != "" {
|
if len(options.Key) > 0 {
|
||||||
key = []byte(options.Key)
|
key = []byte(strings.Join(options.Key, "\n"))
|
||||||
} else if options.KeyPath != "" {
|
} else if options.KeyPath != "" {
|
||||||
content, err := os.ReadFile(options.KeyPath)
|
content, err := os.ReadFile(options.KeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -231,7 +233,7 @@ func NewSTDServer(ctx context.Context, router adapter.Router, logger log.Logger,
|
|||||||
}
|
}
|
||||||
if certificate == nil && key == nil && options.Insecure {
|
if certificate == nil && key == nil && options.Insecure {
|
||||||
tlsConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
tlsConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
return GenerateKeyPair(router.TimeFunc(), info.ServerName)
|
return GenerateKeyPair(ntp.TimeFuncFromContext(ctx), info.ServerName)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if certificate == nil {
|
if certificate == nil {
|
||||||
|
@ -11,9 +11,9 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/ntp"
|
||||||
utls "github.com/sagernet/utls"
|
utls "github.com/sagernet/utls"
|
||||||
|
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
@ -113,7 +113,7 @@ func (c *utlsALPNWrapper) HandshakeContext(ctx context.Context) error {
|
|||||||
return c.UConn.HandshakeContext(ctx)
|
return c.UConn.HandshakeContext(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUTLSClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (*UTLSClientConfig, error) {
|
func NewUTLSClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (*UTLSClientConfig, error) {
|
||||||
var serverName string
|
var serverName string
|
||||||
if options.ServerName != "" {
|
if options.ServerName != "" {
|
||||||
serverName = options.ServerName
|
serverName = options.ServerName
|
||||||
@ -127,7 +127,7 @@ func NewUTLSClient(router adapter.Router, serverAddress string, options option.O
|
|||||||
}
|
}
|
||||||
|
|
||||||
var tlsConfig utls.Config
|
var tlsConfig utls.Config
|
||||||
tlsConfig.Time = router.TimeFunc()
|
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||||
if options.DisableSNI {
|
if options.DisableSNI {
|
||||||
tlsConfig.ServerName = "127.0.0.1"
|
tlsConfig.ServerName = "127.0.0.1"
|
||||||
} else {
|
} else {
|
||||||
|
@ -3,15 +3,16 @@
|
|||||||
package tls
|
package tls
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"context"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewUTLSClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
func NewUTLSClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||||
return nil, E.New(`uTLS is not included in this build, rebuild with -tags with_utls`)
|
return nil, E.New(`uTLS is not included in this build, rebuild with -tags with_utls`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRealityClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
func NewRealityClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||||
return nil, E.New(`uTLS, which is required by reality client is not included in this build, rebuild with -tags with_utls`)
|
return nil, E.New(`uTLS, which is required by reality client is not included in this build, rebuild with -tags with_utls`)
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ const (
|
|||||||
TypeShadowsocksR = "shadowsocksr"
|
TypeShadowsocksR = "shadowsocksr"
|
||||||
TypeVLESS = "vless"
|
TypeVLESS = "vless"
|
||||||
TypeTUIC = "tuic"
|
TypeTUIC = "tuic"
|
||||||
|
TypeHysteria2 = "hysteria2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -65,6 +66,8 @@ func ProxyDisplayName(proxyType string) string {
|
|||||||
return "VLESS"
|
return "VLESS"
|
||||||
case TypeTUIC:
|
case TypeTUIC:
|
||||||
return "TUIC"
|
return "TUIC"
|
||||||
|
case TypeHysteria2:
|
||||||
|
return "Hysteria2"
|
||||||
case TypeSelector:
|
case TypeSelector:
|
||||||
return "Selector"
|
return "Selector"
|
||||||
case TypeURLTest:
|
case TypeURLTest:
|
||||||
|
@ -1,3 +1,48 @@
|
|||||||
|
#### 1.5.0-beta.5
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.5.0-beta.4
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.5.0-beta.3
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
* Updated Hysteria2 documentation **1**
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
Added notes indicating compatibility issues with the official
|
||||||
|
Hysteria2 server and client when using `fastOpen=false` or UDP MTU >= 1200.
|
||||||
|
|
||||||
|
#### 1.5.0-beta.2
|
||||||
|
|
||||||
|
* Add hysteria2 protocol support **1**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
See [Hysteria2 inbound](/configuration/inbound/hysteria2) and [Hysteria2 outbound](/configuration/outbound/hysteria2)
|
||||||
|
|
||||||
|
For protocol description, please refer to [https://v2.hysteria.network](https://v2.hysteria.network)
|
||||||
|
|
||||||
|
#### 1.5.0-beta.1
|
||||||
|
|
||||||
|
* Add TLS [ECH server](/configuration/shared/tls) support
|
||||||
|
* Improve TLS TCH client configuration
|
||||||
|
* Add TLS ECH key pair generator **1**
|
||||||
|
* Add TLS ECH support for QUIC based protocols **2**
|
||||||
|
* Add KDE support for the `set_system_proxy` option in HTTP inbound
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
Command: `sing-box generate ech-keypair <plain_server_name> [-pq-signature-schemes-enabled]`
|
||||||
|
|
||||||
|
**2**:
|
||||||
|
|
||||||
|
All inbounds and outbounds are supported, including `Naiveproxy`, `Hysteria`, `TUIC` and `V2ray QUIC transport`.
|
||||||
|
|
||||||
#### 1.4.1
|
#### 1.4.1
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
89
docs/configuration/inbound/hysteria2.md
Normal file
89
docs/configuration/inbound/hysteria2.md
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
### Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "hysteria2",
|
||||||
|
"tag": "hy2-in",
|
||||||
|
|
||||||
|
... // Listen Fields
|
||||||
|
|
||||||
|
"up_mbps": 100,
|
||||||
|
"down_mbps": 100,
|
||||||
|
"obfs": {
|
||||||
|
"type": "salamander",
|
||||||
|
"password": "cry_me_a_r1ver"
|
||||||
|
},
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"name": "tobyxdd",
|
||||||
|
"password": "goofy_ahh_password"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ignore_client_bandwidth": false,
|
||||||
|
"masquerade": "",
|
||||||
|
"tls": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning "Compatibility issues with the official client"
|
||||||
|
|
||||||
|
The use case of `fastOpen=false` or UDP MTU >= 1200 is not supported when using the official client.
|
||||||
|
|
||||||
|
!!! warning ""
|
||||||
|
|
||||||
|
QUIC, which is required by Hysteria2 is not included by default, see [Installation](/#installation).
|
||||||
|
|
||||||
|
### Listen Fields
|
||||||
|
|
||||||
|
See [Listen Fields](/configuration/shared/listen) for details.
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
#### up_mbps, down_mbps
|
||||||
|
|
||||||
|
Max bandwidth, in Mbps.
|
||||||
|
|
||||||
|
Not limited if empty.
|
||||||
|
|
||||||
|
Conflict with `ignore_client_bandwidth`.
|
||||||
|
|
||||||
|
#### obfs.type
|
||||||
|
|
||||||
|
QUIC traffic obfuscator type, only available with `salamander`.
|
||||||
|
|
||||||
|
Disabled if empty.
|
||||||
|
|
||||||
|
#### obfs.password
|
||||||
|
|
||||||
|
QUIC traffic obfuscator password.
|
||||||
|
|
||||||
|
#### users
|
||||||
|
|
||||||
|
Hysteria2 users
|
||||||
|
|
||||||
|
#### users.password
|
||||||
|
|
||||||
|
Authentication password
|
||||||
|
|
||||||
|
#### ignore_client_bandwidth
|
||||||
|
|
||||||
|
Commands the client to use the BBR flow control algorithm instead of Hysteria CC.
|
||||||
|
|
||||||
|
Conflict with `up_mbps` and `down_mbps`.
|
||||||
|
|
||||||
|
#### masquerade
|
||||||
|
|
||||||
|
HTTP3 server behavior when authentication fails.
|
||||||
|
|
||||||
|
| Scheme | Example | Description |
|
||||||
|
|--------------|-------------------------|--------------------|
|
||||||
|
| `file` | `file:///var/www` | As a file server |
|
||||||
|
| `http/https` | `http://127.0.0.1:8080` | As a reverse proxy |
|
||||||
|
|
||||||
|
A 404 page will be returned if empty.
|
||||||
|
|
||||||
|
#### tls
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
|
87
docs/configuration/inbound/hysteria2.zh.md
Normal file
87
docs/configuration/inbound/hysteria2.zh.md
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
### 结构
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "hysteria2",
|
||||||
|
"tag": "hy2-in",
|
||||||
|
|
||||||
|
... // 监听字段
|
||||||
|
|
||||||
|
"up_mbps": 100,
|
||||||
|
"down_mbps": 100,
|
||||||
|
"obfs": {
|
||||||
|
"type": "salamander",
|
||||||
|
"password": "cry_me_a_r1ver"
|
||||||
|
},
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"name": "tobyxdd",
|
||||||
|
"password": "goofy_ahh_password"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ignore_client_bandwidth": false,
|
||||||
|
"masquerade": "",
|
||||||
|
"tls": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning "与官方客户端的兼容性问题"
|
||||||
|
|
||||||
|
当使用原始客户端时,不支持 `fastOpen=false` 或者 UDP MTU >= 1200 的用例。
|
||||||
|
|
||||||
|
!!! warning ""
|
||||||
|
|
||||||
|
默认安装不包含被 Hysteria2 依赖的 QUIC,参阅 [安装](/zh/#_2)。
|
||||||
|
|
||||||
|
### 监听字段
|
||||||
|
|
||||||
|
参阅 [监听字段](/zh/configuration/shared/listen/)。
|
||||||
|
|
||||||
|
### 字段
|
||||||
|
|
||||||
|
#### up_mbps, down_mbps
|
||||||
|
|
||||||
|
支持的速率,默认不限制。
|
||||||
|
|
||||||
|
与 `ignore_client_bandwidth` 冲突。
|
||||||
|
|
||||||
|
#### obfs.type
|
||||||
|
|
||||||
|
QUIC 流量混淆器类型,仅可设为 `salamander`。
|
||||||
|
|
||||||
|
如果为空则禁用。
|
||||||
|
|
||||||
|
#### obfs.password
|
||||||
|
|
||||||
|
QUIC 流量混淆器密码.
|
||||||
|
|
||||||
|
#### users
|
||||||
|
|
||||||
|
Hysteria 用户
|
||||||
|
|
||||||
|
#### users.password
|
||||||
|
|
||||||
|
认证密码。
|
||||||
|
|
||||||
|
#### ignore_client_bandwidth
|
||||||
|
|
||||||
|
命令客户端使用 BBR 流量控制算法而不是 Hysteria CC。
|
||||||
|
|
||||||
|
与 `up_mbps` 和 `down_mbps` 冲突。
|
||||||
|
|
||||||
|
#### masquerade
|
||||||
|
|
||||||
|
HTTP3 服务器认证失败时的行为。
|
||||||
|
|
||||||
|
| Scheme | 示例 | 描述 |
|
||||||
|
|--------------|-------------------------|---------|
|
||||||
|
| `file` | `file:///var/www` | 作为文件服务器 |
|
||||||
|
| `http/https` | `http://127.0.0.1:8080` | 作为反向代理 |
|
||||||
|
|
||||||
|
如果为空,则返回 404 页。
|
||||||
|
|
||||||
|
#### tls
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
|
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
@ -27,6 +27,8 @@
|
|||||||
| `naive` | [Naive](./naive) | X |
|
| `naive` | [Naive](./naive) | X |
|
||||||
| `hysteria` | [Hysteria](./hysteria) | X |
|
| `hysteria` | [Hysteria](./hysteria) | X |
|
||||||
| `shadowtls` | [ShadowTLS](./shadowtls) | TCP |
|
| `shadowtls` | [ShadowTLS](./shadowtls) | TCP |
|
||||||
|
| `tuic` | [TUIC](./tuic) | X |
|
||||||
|
| `hysteria2` | [Hysteria2](./hysteria2) | X |
|
||||||
| `vless` | [VLESS](./vless) | TCP |
|
| `vless` | [VLESS](./vless) | TCP |
|
||||||
| `tun` | [Tun](./tun) | X |
|
| `tun` | [Tun](./tun) | X |
|
||||||
| `redirect` | [Redirect](./redirect) | X |
|
| `redirect` | [Redirect](./redirect) | X |
|
||||||
|
@ -26,6 +26,10 @@
|
|||||||
| `trojan` | [Trojan](./trojan) | TCP |
|
| `trojan` | [Trojan](./trojan) | TCP |
|
||||||
| `naive` | [Naive](./naive) | X |
|
| `naive` | [Naive](./naive) | X |
|
||||||
| `hysteria` | [Hysteria](./hysteria) | X |
|
| `hysteria` | [Hysteria](./hysteria) | X |
|
||||||
|
| `shadowtls` | [ShadowTLS](./shadowtls) | TCP |
|
||||||
|
| `tuic` | [TUIC](./tuic) | X |
|
||||||
|
| `hysteria2` | [Hysteria2](./hysteria2) | X |
|
||||||
|
| `vless` | [VLESS](./vless) | TCP |
|
||||||
| `tun` | [Tun](./tun) | X |
|
| `tun` | [Tun](./tun) | X |
|
||||||
| `redirect` | [Redirect](./redirect) | X |
|
| `redirect` | [Redirect](./redirect) | X |
|
||||||
| `tproxy` | [TProxy](./tproxy) | X |
|
| `tproxy` | [TProxy](./tproxy) | X |
|
||||||
|
82
docs/configuration/outbound/hysteria2.md
Normal file
82
docs/configuration/outbound/hysteria2.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
### Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "hysteria2",
|
||||||
|
"tag": "hy2-out",
|
||||||
|
|
||||||
|
"server": "127.0.0.1",
|
||||||
|
"server_port": 1080,
|
||||||
|
"up_mbps": 100,
|
||||||
|
"down_mbps": 100,
|
||||||
|
"obfs": {
|
||||||
|
"type": "salamander",
|
||||||
|
"password": "cry_me_a_r1ver"
|
||||||
|
},
|
||||||
|
"password": "goofy_ahh_password",
|
||||||
|
"network": "tcp",
|
||||||
|
"tls": {},
|
||||||
|
|
||||||
|
... // Dial Fields
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning "Compatibility issues with the official server"
|
||||||
|
|
||||||
|
The use case of UDP MTU >= 1200 is not supported when using the official server.
|
||||||
|
|
||||||
|
!!! warning ""
|
||||||
|
|
||||||
|
QUIC, which is required by Hysteria2 is not included by default, see [Installation](/#installation).
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
#### server
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
The server address.
|
||||||
|
|
||||||
|
#### server_port
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
The server port.
|
||||||
|
|
||||||
|
#### up_mbps, down_mbps
|
||||||
|
|
||||||
|
Max bandwidth, in Mbps.
|
||||||
|
|
||||||
|
If empty, the BBR congestion control algorithm will be used instead of Hysteria CC.
|
||||||
|
|
||||||
|
#### obfs.type
|
||||||
|
|
||||||
|
QUIC traffic obfuscator type, only available with `salamander`.
|
||||||
|
|
||||||
|
Disabled if empty.
|
||||||
|
|
||||||
|
#### obfs.password
|
||||||
|
|
||||||
|
QUIC traffic obfuscator password.
|
||||||
|
|
||||||
|
#### password
|
||||||
|
|
||||||
|
Authentication password.
|
||||||
|
|
||||||
|
#### network
|
||||||
|
|
||||||
|
Enabled network
|
||||||
|
|
||||||
|
One of `tcp` `udp`.
|
||||||
|
|
||||||
|
Both is enabled by default.
|
||||||
|
|
||||||
|
#### tls
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||||
|
|
||||||
|
### Dial Fields
|
||||||
|
|
||||||
|
See [Dial Fields](/configuration/shared/dial) for details.
|
83
docs/configuration/outbound/hysteria2.zh.md
Normal file
83
docs/configuration/outbound/hysteria2.zh.md
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
### 结构
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "hysteria2",
|
||||||
|
"tag": "hy2-out",
|
||||||
|
|
||||||
|
"server": "127.0.0.1",
|
||||||
|
"server_port": 1080,
|
||||||
|
"up_mbps": 100,
|
||||||
|
"down_mbps": 100,
|
||||||
|
"obfs": {
|
||||||
|
"type": "salamander",
|
||||||
|
"password": "cry_me_a_r1ver"
|
||||||
|
},
|
||||||
|
"password": "goofy_ahh_password",
|
||||||
|
"network": "tcp",
|
||||||
|
"tls": {},
|
||||||
|
|
||||||
|
... // 拨号字段
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning "与官方服务器的兼容性问题"
|
||||||
|
|
||||||
|
当使用原始服务器时,不支持 UDP MTU >= 1200 的用例。
|
||||||
|
|
||||||
|
!!! warning ""
|
||||||
|
|
||||||
|
默认安装不包含被 Hysteria2 依赖的 QUIC,参阅 [安装](/zh/#_2)。
|
||||||
|
|
||||||
|
### 字段
|
||||||
|
|
||||||
|
#### server
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
|
服务器地址。
|
||||||
|
|
||||||
|
#### server_port
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
|
服务器端口。
|
||||||
|
|
||||||
|
#### up_mbps, down_mbps
|
||||||
|
|
||||||
|
最大带宽。
|
||||||
|
|
||||||
|
如果为空,将使用 BBR 流量控制算法而不是 Hysteria CC。
|
||||||
|
|
||||||
|
#### obfs.type
|
||||||
|
|
||||||
|
QUIC 流量混淆器类型,仅可设为 `salamander`。
|
||||||
|
|
||||||
|
如果为空则禁用。
|
||||||
|
|
||||||
|
#### obfs.password
|
||||||
|
|
||||||
|
QUIC 流量混淆器密码.
|
||||||
|
|
||||||
|
#### password
|
||||||
|
|
||||||
|
认证密码。
|
||||||
|
|
||||||
|
#### network
|
||||||
|
|
||||||
|
启用的网络协议。
|
||||||
|
|
||||||
|
`tcp` 或 `udp`。
|
||||||
|
|
||||||
|
默认所有。
|
||||||
|
|
||||||
|
#### tls
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
|
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||||
|
|
||||||
|
|
||||||
|
### 拨号字段
|
||||||
|
|
||||||
|
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
@ -29,6 +29,8 @@
|
|||||||
| `shadowsocksr` | [ShadowsocksR](./shadowsocksr) |
|
| `shadowsocksr` | [ShadowsocksR](./shadowsocksr) |
|
||||||
| `vless` | [VLESS](./vless) |
|
| `vless` | [VLESS](./vless) |
|
||||||
| `shadowtls` | [ShadowTLS](./shadowtls) |
|
| `shadowtls` | [ShadowTLS](./shadowtls) |
|
||||||
|
| `tuic` | [TUIC](./tuic) |
|
||||||
|
| `hysteria2` | [Hysteria2](./hysteria2) |
|
||||||
| `tor` | [Tor](./tor) |
|
| `tor` | [Tor](./tor) |
|
||||||
| `ssh` | [SSH](./ssh) |
|
| `ssh` | [SSH](./ssh) |
|
||||||
| `dns` | [DNS](./dns) |
|
| `dns` | [DNS](./dns) |
|
||||||
|
@ -28,6 +28,9 @@
|
|||||||
| `hysteria` | [Hysteria](./hysteria) |
|
| `hysteria` | [Hysteria](./hysteria) |
|
||||||
| `shadowsocksr` | [ShadowsocksR](./shadowsocksr) |
|
| `shadowsocksr` | [ShadowsocksR](./shadowsocksr) |
|
||||||
| `vless` | [VLESS](./vless) |
|
| `vless` | [VLESS](./vless) |
|
||||||
|
| `shadowtls` | [ShadowTLS](./shadowtls) |
|
||||||
|
| `tuic` | [TUIC](./tuic) |
|
||||||
|
| `hysteria2` | [Hysteria2](./hysteria2) |
|
||||||
| `tor` | [Tor](./tor) |
|
| `tor` | [Tor](./tor) |
|
||||||
| `ssh` | [SSH](./ssh) |
|
| `ssh` | [SSH](./ssh) |
|
||||||
| `dns` | [DNS](./dns) |
|
| `dns` | [DNS](./dns) |
|
||||||
|
@ -8,9 +8,9 @@
|
|||||||
"min_version": "",
|
"min_version": "",
|
||||||
"max_version": "",
|
"max_version": "",
|
||||||
"cipher_suites": [],
|
"cipher_suites": [],
|
||||||
"certificate": "",
|
"certificate": [],
|
||||||
"certificate_path": "",
|
"certificate_path": "",
|
||||||
"key": "",
|
"key": [],
|
||||||
"key_path": "",
|
"key_path": "",
|
||||||
"acme": {
|
"acme": {
|
||||||
"domain": [],
|
"domain": [],
|
||||||
@ -27,6 +27,13 @@
|
|||||||
"mac_key": ""
|
"mac_key": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ech": {
|
||||||
|
"enabled": false,
|
||||||
|
"pq_signature_schemes_enabled": false,
|
||||||
|
"dynamic_record_sizing_disabled": false,
|
||||||
|
"key": [],
|
||||||
|
"key_path": ""
|
||||||
|
},
|
||||||
"reality": {
|
"reality": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"handshake": {
|
"handshake": {
|
||||||
@ -62,7 +69,8 @@
|
|||||||
"enabled": false,
|
"enabled": false,
|
||||||
"pq_signature_schemes_enabled": false,
|
"pq_signature_schemes_enabled": false,
|
||||||
"dynamic_record_sizing_disabled": false,
|
"dynamic_record_sizing_disabled": false,
|
||||||
"config": ""
|
"config": [],
|
||||||
|
"config_path": ""
|
||||||
},
|
},
|
||||||
"utls": {
|
"utls": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
@ -162,7 +170,7 @@ This may change in the future.
|
|||||||
|
|
||||||
#### certificate
|
#### certificate
|
||||||
|
|
||||||
The server certificate, in PEM format.
|
The server certificate line array, in PEM format.
|
||||||
|
|
||||||
#### certificate_path
|
#### certificate_path
|
||||||
|
|
||||||
@ -172,7 +180,7 @@ The path to the server certificate, in PEM format.
|
|||||||
|
|
||||||
==Server only==
|
==Server only==
|
||||||
|
|
||||||
The server private key, in PEM format.
|
The server private key line array, in PEM format.
|
||||||
|
|
||||||
#### key_path
|
#### key_path
|
||||||
|
|
||||||
@ -180,18 +188,11 @@ The server private key, in PEM format.
|
|||||||
|
|
||||||
The path to the server private key, in PEM format.
|
The path to the server private key, in PEM format.
|
||||||
|
|
||||||
#### ech
|
## Custom TLS support
|
||||||
|
|
||||||
==Client only==
|
!!! info "QUIC support"
|
||||||
|
|
||||||
!!! warning ""
|
Only ECH is supported in QUIC.
|
||||||
|
|
||||||
ECH is not included by default, see [Installation](/#installation).
|
|
||||||
|
|
||||||
ECH (Encrypted Client Hello) is a TLS extension that allows a client to encrypt the first part of its ClientHello
|
|
||||||
message.
|
|
||||||
|
|
||||||
If you don't know how to fill in the other configuration, just set `enabled`.
|
|
||||||
|
|
||||||
#### utls
|
#### utls
|
||||||
|
|
||||||
@ -222,6 +223,58 @@ Available fingerprint values:
|
|||||||
|
|
||||||
Chrome fingerprint will be used if empty.
|
Chrome fingerprint will be used if empty.
|
||||||
|
|
||||||
|
### ECH Fields
|
||||||
|
|
||||||
|
!!! warning ""
|
||||||
|
|
||||||
|
ECH is not included by default, see [Installation](/#installation).
|
||||||
|
|
||||||
|
ECH (Encrypted Client Hello) is a TLS extension that allows a client to encrypt the first part of its ClientHello
|
||||||
|
message.
|
||||||
|
|
||||||
|
The ECH key and configuration can be generated by `sing-box generate ech-keypair [-pq-signature-schemes-enabled]`.
|
||||||
|
|
||||||
|
#### pq_signature_schemes_enabled
|
||||||
|
|
||||||
|
Enable support for post-quantum peer certificate signature schemes.
|
||||||
|
|
||||||
|
It is recommended to match the parameters of `sing-box generate ech-keypair`.
|
||||||
|
|
||||||
|
#### dynamic_record_sizing_disabled
|
||||||
|
|
||||||
|
Disables adaptive sizing of TLS records.
|
||||||
|
|
||||||
|
When true, the largest possible TLS record size is always used.
|
||||||
|
When false, the size of TLS records may be adjusted in an attempt to improve latency.
|
||||||
|
|
||||||
|
#### key
|
||||||
|
|
||||||
|
==Server only==
|
||||||
|
|
||||||
|
ECH key line array, in PEM format.
|
||||||
|
|
||||||
|
#### key_path
|
||||||
|
|
||||||
|
==Server only==
|
||||||
|
|
||||||
|
The path to ECH key, in PEM format.
|
||||||
|
|
||||||
|
#### config
|
||||||
|
|
||||||
|
==Client only==
|
||||||
|
|
||||||
|
ECH configuration line array, in PEM format.
|
||||||
|
|
||||||
|
If empty, load from DNS will be attempted.
|
||||||
|
|
||||||
|
#### config_path
|
||||||
|
|
||||||
|
==Client only==
|
||||||
|
|
||||||
|
The path to ECH configuration, in PEM format.
|
||||||
|
|
||||||
|
If empty, load from DNS will be attempted.
|
||||||
|
|
||||||
### ACME Fields
|
### ACME Fields
|
||||||
|
|
||||||
!!! warning ""
|
!!! warning ""
|
||||||
@ -345,4 +398,4 @@ Check disabled if empty.
|
|||||||
|
|
||||||
### Reload
|
### Reload
|
||||||
|
|
||||||
For server configuration, certificate and key will be automatically reloaded if modified.
|
For server configuration, certificate, key and ECH key will be automatically reloaded if modified.
|
@ -8,9 +8,9 @@
|
|||||||
"min_version": "",
|
"min_version": "",
|
||||||
"max_version": "",
|
"max_version": "",
|
||||||
"cipher_suites": [],
|
"cipher_suites": [],
|
||||||
"certificate": "",
|
"certificate": [],
|
||||||
"certificate_path": "",
|
"certificate_path": "",
|
||||||
"key": "",
|
"key": [],
|
||||||
"key_path": "",
|
"key_path": "",
|
||||||
"acme": {
|
"acme": {
|
||||||
"domain": [],
|
"domain": [],
|
||||||
@ -27,6 +27,13 @@
|
|||||||
"mac_key": ""
|
"mac_key": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ech": {
|
||||||
|
"enabled": false,
|
||||||
|
"pq_signature_schemes_enabled": false,
|
||||||
|
"dynamic_record_sizing_disabled": false,
|
||||||
|
"key": [],
|
||||||
|
"key_path": ""
|
||||||
|
},
|
||||||
"reality": {
|
"reality": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"handshake": {
|
"handshake": {
|
||||||
@ -56,13 +63,14 @@
|
|||||||
"min_version": "",
|
"min_version": "",
|
||||||
"max_version": "",
|
"max_version": "",
|
||||||
"cipher_suites": [],
|
"cipher_suites": [],
|
||||||
"certificate": "",
|
"certificate": [],
|
||||||
"certificate_path": "",
|
"certificate_path": "",
|
||||||
"ech": {
|
"ech": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"pq_signature_schemes_enabled": false,
|
"pq_signature_schemes_enabled": false,
|
||||||
"dynamic_record_sizing_disabled": false,
|
"dynamic_record_sizing_disabled": false,
|
||||||
"config": ""
|
"config": [],
|
||||||
|
"config_path": ""
|
||||||
},
|
},
|
||||||
"utls": {
|
"utls": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
@ -162,7 +170,7 @@ TLS 版本值:
|
|||||||
|
|
||||||
#### certificate
|
#### certificate
|
||||||
|
|
||||||
服务器 PEM 证书。
|
服务器 PEM 证书行数组。
|
||||||
|
|
||||||
#### certificate_path
|
#### certificate_path
|
||||||
|
|
||||||
@ -172,7 +180,7 @@ TLS 版本值:
|
|||||||
|
|
||||||
==仅服务器==
|
==仅服务器==
|
||||||
|
|
||||||
服务器 PEM 私钥。
|
服务器 PEM 私钥行数组。
|
||||||
|
|
||||||
#### key_path
|
#### key_path
|
||||||
|
|
||||||
@ -180,19 +188,6 @@ TLS 版本值:
|
|||||||
|
|
||||||
服务器 PEM 私钥路径。
|
服务器 PEM 私钥路径。
|
||||||
|
|
||||||
#### ech
|
|
||||||
|
|
||||||
==仅客户端==
|
|
||||||
|
|
||||||
!!! warning ""
|
|
||||||
|
|
||||||
默认安装不包含 ECH, 参阅 [安装](/zh/#_2)。
|
|
||||||
|
|
||||||
ECH (Encrypted Client Hello) 是一个 TLS 扩展,它允许客户端加密其 ClientHello 的第一部分
|
|
||||||
信息。
|
|
||||||
|
|
||||||
如果您不知道如何填写其他配置,只需设置 `enabled` 即可。
|
|
||||||
|
|
||||||
#### utls
|
#### utls
|
||||||
|
|
||||||
==仅客户端==
|
==仅客户端==
|
||||||
@ -222,6 +217,59 @@ uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻
|
|||||||
|
|
||||||
默认使用 chrome 指纹。
|
默认使用 chrome 指纹。
|
||||||
|
|
||||||
|
## ECH 字段
|
||||||
|
|
||||||
|
!!! warning ""
|
||||||
|
|
||||||
|
默认安装不包含 ECH, 参阅 [安装](/zh/#_2)。
|
||||||
|
|
||||||
|
ECH (Encrypted Client Hello) 是一个 TLS 扩展,它允许客户端加密其 ClientHello 的第一部分
|
||||||
|
信息。
|
||||||
|
|
||||||
|
|
||||||
|
ECH 配置和密钥可以通过 `sing-box generate ech-keypair [-pq-signature-schemes-enabled]` 生成。
|
||||||
|
|
||||||
|
#### pq_signature_schemes_enabled
|
||||||
|
|
||||||
|
启用对后量子对等证书签名方案的支持。
|
||||||
|
|
||||||
|
建议匹配 `sing-box generate ech-keypair` 的参数。
|
||||||
|
|
||||||
|
#### dynamic_record_sizing_disabled
|
||||||
|
|
||||||
|
禁用 TLS 记录的自适应大小调整。
|
||||||
|
|
||||||
|
如果为 true,则始终使用最大可能的 TLS 记录大小。
|
||||||
|
如果为 false,则可能会调整 TLS 记录的大小以尝试改善延迟。
|
||||||
|
|
||||||
|
#### key
|
||||||
|
|
||||||
|
==仅服务器==
|
||||||
|
|
||||||
|
ECH PEM 密钥行数组
|
||||||
|
|
||||||
|
#### key_path
|
||||||
|
|
||||||
|
==仅服务器==
|
||||||
|
|
||||||
|
ECH PEM 密钥路径
|
||||||
|
|
||||||
|
#### config
|
||||||
|
|
||||||
|
==仅客户端==
|
||||||
|
|
||||||
|
ECH PEM 配置行数组
|
||||||
|
|
||||||
|
如果为空,将尝试从 DNS 加载。
|
||||||
|
|
||||||
|
#### config_path
|
||||||
|
|
||||||
|
==仅客户端==
|
||||||
|
|
||||||
|
ECH PEM 配置路径
|
||||||
|
|
||||||
|
如果为空,将尝试从 DNS 加载。
|
||||||
|
|
||||||
### ACME 字段
|
### ACME 字段
|
||||||
|
|
||||||
!!! warning ""
|
!!! warning ""
|
||||||
|
25
go.mod
25
go.mod
@ -6,6 +6,7 @@ require (
|
|||||||
berty.tech/go-libtor v1.0.385
|
berty.tech/go-libtor v1.0.385
|
||||||
github.com/Dreamacro/clash v1.17.0
|
github.com/Dreamacro/clash v1.17.0
|
||||||
github.com/caddyserver/certmagic v0.19.2
|
github.com/caddyserver/certmagic v0.19.2
|
||||||
|
github.com/cloudflare/circl v1.3.3
|
||||||
github.com/cretz/bine v0.2.0
|
github.com/cretz/bine v0.2.0
|
||||||
github.com/dustin/go-humanize v1.0.1
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/fsnotify/fsnotify v1.6.0
|
github.com/fsnotify/fsnotify v1.6.0
|
||||||
@ -13,17 +14,17 @@ require (
|
|||||||
github.com/go-chi/cors v1.2.1
|
github.com/go-chi/cors v1.2.1
|
||||||
github.com/go-chi/render v1.0.3
|
github.com/go-chi/render v1.0.3
|
||||||
github.com/gofrs/uuid/v5 v5.0.0
|
github.com/gofrs/uuid/v5 v5.0.0
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20230816195147-b3ca2534940d
|
github.com/insomniacslk/dhcp v0.0.0-20230906122924-c71a6be05968
|
||||||
github.com/logrusorgru/aurora v2.0.3+incompatible
|
github.com/logrusorgru/aurora v2.0.3+incompatible
|
||||||
github.com/mholt/acmez v1.2.0
|
github.com/mholt/acmez v1.2.0
|
||||||
github.com/miekg/dns v1.1.55
|
github.com/miekg/dns v1.1.55
|
||||||
github.com/ooni/go-libtor v1.1.8
|
github.com/ooni/go-libtor v1.1.8
|
||||||
github.com/oschwald/maxminddb-golang v1.12.0
|
github.com/oschwald/maxminddb-golang v1.12.0
|
||||||
github.com/pires/go-proxyproto v0.7.0
|
github.com/pires/go-proxyproto v0.7.0
|
||||||
github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0
|
github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a
|
||||||
github.com/sagernet/gomobile v0.0.0-20230728014906-3de089147f59
|
github.com/sagernet/gomobile v0.0.0-20230728014906-3de089147f59
|
||||||
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2
|
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2
|
||||||
github.com/sagernet/quic-go v0.0.0-20230825040534-0cd917b2ddda
|
github.com/sagernet/quic-go v0.0.0-20230831052420-45809eee2e86
|
||||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
|
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
|
||||||
github.com/sagernet/sing v0.2.10-0.20230907044649-03c21c0a1205
|
github.com/sagernet/sing v0.2.10-0.20230907044649-03c21c0a1205
|
||||||
github.com/sagernet/sing-dns v0.1.9-0.20230824120133-4d5cbceb40c1
|
github.com/sagernet/sing-dns v0.1.9-0.20230824120133-4d5cbceb40c1
|
||||||
@ -43,12 +44,12 @@ require (
|
|||||||
go.etcd.io/bbolt v1.3.7
|
go.etcd.io/bbolt v1.3.7
|
||||||
go.uber.org/zap v1.25.0
|
go.uber.org/zap v1.25.0
|
||||||
go4.org/netipx v0.0.0-20230824141953-6213f710f925
|
go4.org/netipx v0.0.0-20230824141953-6213f710f925
|
||||||
golang.org/x/crypto v0.12.0
|
golang.org/x/crypto v0.13.0
|
||||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
|
||||||
golang.org/x/net v0.14.0
|
golang.org/x/net v0.15.0
|
||||||
golang.org/x/sys v0.11.0
|
golang.org/x/sys v0.12.0
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||||
google.golang.org/grpc v1.57.0
|
google.golang.org/grpc v1.58.0
|
||||||
google.golang.org/protobuf v1.31.0
|
google.golang.org/protobuf v1.31.0
|
||||||
howett.net/plist v1.0.0
|
howett.net/plist v1.0.0
|
||||||
)
|
)
|
||||||
@ -59,11 +60,9 @@ require (
|
|||||||
github.com/Dreamacro/protobytes v0.0.0-20230617041236-6500a9f4f158 // indirect
|
github.com/Dreamacro/protobytes v0.0.0-20230617041236-6500a9f4f158 // indirect
|
||||||
github.com/ajg/form v1.5.1 // indirect
|
github.com/ajg/form v1.5.1 // indirect
|
||||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||||
github.com/cloudflare/circl v1.2.1-0.20221019164342-6ab4dfed8f3c // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||||
github.com/golang/mock v1.6.0 // indirect
|
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/google/btree v1.1.2 // indirect
|
github.com/google/btree v1.1.2 // indirect
|
||||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||||
@ -88,10 +87,10 @@ require (
|
|||||||
github.com/zeebo/blake3 v0.2.3 // indirect
|
github.com/zeebo/blake3 v0.2.3 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/mod v0.12.0 // indirect
|
golang.org/x/mod v0.12.0 // indirect
|
||||||
golang.org/x/text v0.12.0 // indirect
|
golang.org/x/text v0.13.0 // indirect
|
||||||
golang.org/x/time v0.3.0 // indirect
|
golang.org/x/time v0.3.0 // indirect
|
||||||
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect
|
golang.org/x/tools v0.13.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
lukechampine.com/blake3 v1.2.1 // indirect
|
lukechampine.com/blake3 v1.2.1 // indirect
|
||||||
|
71
go.sum
71
go.sum
@ -14,8 +14,8 @@ github.com/caddyserver/certmagic v0.19.2/go.mod h1:fsL01NomQ6N+kE2j37ZCnig2MFosG
|
|||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
github.com/cloudflare/circl v1.2.1-0.20221019164342-6ab4dfed8f3c h1:K1VdSnBZiGapczwcUKnE1qcsMBclA84DUOD2NG/78VY=
|
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
|
||||||
github.com/cloudflare/circl v1.2.1-0.20221019164342-6ab4dfed8f3c/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw=
|
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw=
|
github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw=
|
||||||
github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
|
github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
|
||||||
@ -40,8 +40,6 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEe
|
|||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||||
github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
|
github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
|
||||||
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
|
||||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
@ -56,8 +54,8 @@ github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbg
|
|||||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20230816195147-b3ca2534940d h1:Ka64cclWedOkGzm9M2/XYuwJUdmWRUozmsxW0PyKA3A=
|
github.com/insomniacslk/dhcp v0.0.0-20230906122924-c71a6be05968 h1:uBiv5/8x42J7myumCdFuDOc5HnEXRK6eOtefwvE6+TQ=
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20230816195147-b3ca2534940d/go.mod h1:7474bZ1YNCvarT6WFKie4kEET6J0KYRDC4XJqqXzQW4=
|
github.com/insomniacslk/dhcp v0.0.0-20230906122924-c71a6be05968/go.mod h1:zmdm3sTSDP3vOOX3CEWRkkRHtKr1DxBx+J1OQFoDQQs=
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||||
@ -98,8 +96,8 @@ github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1
|
|||||||
github.com/quic-go/qtls-go1-20 v0.3.3 h1:17/glZSLI9P9fDAeyCHBFSWSqJcwx1byhLwP5eUIDCM=
|
github.com/quic-go/qtls-go1-20 v0.3.3 h1:17/glZSLI9P9fDAeyCHBFSWSqJcwx1byhLwP5eUIDCM=
|
||||||
github.com/quic-go/qtls-go1-20 v0.3.3/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
github.com/quic-go/qtls-go1-20 v0.3.3/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0 h1:KyhtFFt1Jtp5vW2ohNvstvQffTOQ/s5vENuGXzdA+TM=
|
github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a h1:wZHruBxZCsQLXHAozWpnJBL3wJ/XufDpz0qKtgpSnA4=
|
||||||
github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0/go.mod h1:D4SFEOkJK+4W1v86ZhX0jPM0rAL498fyQAChqMtes/I=
|
github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a/go.mod h1:dNV1ZP9y3qx5ltULeKaQZTZWTLHflgW5DES+Ses7cMI=
|
||||||
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA=
|
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA=
|
||||||
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h1:QUQ4RRHD6hGGHdFMEtR8T2P6GS6R3D/CXKdaYHKKXms=
|
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h1:QUQ4RRHD6hGGHdFMEtR8T2P6GS6R3D/CXKdaYHKKXms=
|
||||||
github.com/sagernet/gomobile v0.0.0-20230728014906-3de089147f59 h1:vN4divY6LYHcYmiTsCHNPmGZtEsEKJzh81LyvgAQfEQ=
|
github.com/sagernet/gomobile v0.0.0-20230728014906-3de089147f59 h1:vN4divY6LYHcYmiTsCHNPmGZtEsEKJzh81LyvgAQfEQ=
|
||||||
@ -108,8 +106,8 @@ github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2 h1:dnkKrzapqtAwjTS
|
|||||||
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2/go.mod h1:1JUiV7nGuf++YFm9eWZ8q2lrwHmhcUGzptMl/vL1+LA=
|
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2/go.mod h1:1JUiV7nGuf++YFm9eWZ8q2lrwHmhcUGzptMl/vL1+LA=
|
||||||
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
|
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
|
||||||
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
||||||
github.com/sagernet/quic-go v0.0.0-20230825040534-0cd917b2ddda h1:7J/hnOFqCThiCrVpvr0wKO+Dic/XPSulPr5yI8FVJMs=
|
github.com/sagernet/quic-go v0.0.0-20230831052420-45809eee2e86 h1:g4TEg9inAtA1FDTXpNrvmx72nN5mTOLQrJce6fVxF9g=
|
||||||
github.com/sagernet/quic-go v0.0.0-20230825040534-0cd917b2ddda/go.mod h1:Iw8Tt3dMqC/61cMHa0nN5i/958oYuuMnQCMOSPx+xcg=
|
github.com/sagernet/quic-go v0.0.0-20230831052420-45809eee2e86/go.mod h1:O4Cj7TmMOvqD6S0XMqJRZfcYzA3m0H0ARbbaJFB0p7A=
|
||||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
|
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
|
||||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
|
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
|
||||||
github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
|
github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
|
||||||
@ -156,7 +154,6 @@ github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gV
|
|||||||
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
|
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
|
||||||
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg=
|
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg=
|
||||||
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
|
||||||
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
||||||
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||||
github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
|
github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
|
||||||
@ -172,67 +169,49 @@ go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c=
|
|||||||
go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk=
|
go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk=
|
||||||
go4.org/netipx v0.0.0-20230824141953-6213f710f925 h1:eeQDDVKFkx0g4Hyy8pHgmZaK0EqB4SD6rvKbUdN3ziQ=
|
go4.org/netipx v0.0.0-20230824141953-6213f710f925 h1:eeQDDVKFkx0g4Hyy8pHgmZaK0EqB4SD6rvKbUdN3ziQ=
|
||||||
go4.org/netipx v0.0.0-20230824141953-6213f710f925/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
go4.org/netipx v0.0.0-20230824141953-6213f710f925/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||||
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
||||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
|
||||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
||||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
|
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
|
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
||||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E=
|
|
||||||
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
|
||||||
google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
|
google.golang.org/grpc v1.58.0 h1:32JY8YpPMSR45K+c3o6b8VL73V+rR8k+DeMIr4vRH8o=
|
||||||
google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
|
google.golang.org/grpc v1.58.0/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||||
|
@ -46,6 +46,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o
|
|||||||
return NewVLESS(ctx, router, logger, options.Tag, options.VLESSOptions)
|
return NewVLESS(ctx, router, logger, options.Tag, options.VLESSOptions)
|
||||||
case C.TypeTUIC:
|
case C.TypeTUIC:
|
||||||
return NewTUIC(ctx, router, logger, options.Tag, options.TUICOptions)
|
return NewTUIC(ctx, router, logger, options.Tag, options.TUICOptions)
|
||||||
|
case C.TypeHysteria2:
|
||||||
|
return NewHysteria2(ctx, router, logger, options.Tag, options.Hysteria2Options)
|
||||||
default:
|
default:
|
||||||
return nil, E.New("unknown inbound type: ", options.Type)
|
return nil, E.New("unknown inbound type: ", options.Type)
|
||||||
}
|
}
|
||||||
|
@ -33,8 +33,8 @@ type myInboundAdapter struct {
|
|||||||
|
|
||||||
// http mixed
|
// http mixed
|
||||||
|
|
||||||
setSystemProxy bool
|
setSystemProxy bool
|
||||||
clearSystemProxy func() error
|
systemProxy settings.SystemProxy
|
||||||
|
|
||||||
// internal
|
// internal
|
||||||
|
|
||||||
@ -91,7 +91,19 @@ func (a *myInboundAdapter) Start() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if a.setSystemProxy {
|
if a.setSystemProxy {
|
||||||
a.clearSystemProxy, err = settings.SetSystemProxy(a.router, M.SocksaddrFromNet(a.tcpListener.Addr()).Port, a.protocol == C.TypeMixed)
|
listenPort := M.SocksaddrFromNet(a.tcpListener.Addr()).Port
|
||||||
|
var listenAddrString string
|
||||||
|
listenAddr := a.listenOptions.Listen.Build()
|
||||||
|
if listenAddr.IsUnspecified() {
|
||||||
|
listenAddrString = "127.0.0.1"
|
||||||
|
} else {
|
||||||
|
listenAddrString = listenAddr.String()
|
||||||
|
}
|
||||||
|
a.systemProxy, err = settings.NewSystemProxy(a.ctx, M.ParseSocksaddrHostPort(listenAddrString, listenPort), a.protocol == C.TypeMixed)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "initialize system proxy")
|
||||||
|
}
|
||||||
|
err = a.systemProxy.Enable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "set system proxy")
|
return E.Cause(err, "set system proxy")
|
||||||
}
|
}
|
||||||
@ -102,8 +114,8 @@ func (a *myInboundAdapter) Start() error {
|
|||||||
func (a *myInboundAdapter) Close() error {
|
func (a *myInboundAdapter) Close() error {
|
||||||
a.inShutdown.Store(true)
|
a.inShutdown.Store(true)
|
||||||
var err error
|
var err error
|
||||||
if a.clearSystemProxy != nil {
|
if a.systemProxy != nil && a.systemProxy.IsEnabled() {
|
||||||
err = a.clearSystemProxy()
|
err = a.systemProxy.Disable()
|
||||||
}
|
}
|
||||||
return E.Errors(err, common.Close(
|
return E.Errors(err, common.Close(
|
||||||
a.tcpListener,
|
a.tcpListener,
|
||||||
|
@ -44,7 +44,7 @@ func NewHTTP(ctx context.Context, router adapter.Router, logger log.ContextLogge
|
|||||||
authenticator: auth.NewAuthenticator(options.Users),
|
authenticator: auth.NewAuthenticator(options.Users),
|
||||||
}
|
}
|
||||||
if options.TLS != nil {
|
if options.TLS != nil {
|
||||||
tlsConfig, err := tls.NewServer(ctx, router, logger, common.PtrValueOrDefault(options.TLS))
|
tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/sagernet/quic-go"
|
"github.com/sagernet/quic-go"
|
||||||
"github.com/sagernet/quic-go/congestion"
|
"github.com/sagernet/quic-go/congestion"
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/qtls"
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
@ -35,7 +36,7 @@ type Hysteria struct {
|
|||||||
xplusKey []byte
|
xplusKey []byte
|
||||||
sendBPS uint64
|
sendBPS uint64
|
||||||
recvBPS uint64
|
recvBPS uint64
|
||||||
listener *quic.Listener
|
listener qtls.QUICListener
|
||||||
udpAccess sync.RWMutex
|
udpAccess sync.RWMutex
|
||||||
udpSessionId uint32
|
udpSessionId uint32
|
||||||
udpSessions map[uint32]chan *hysteria.UDPMessage
|
udpSessions map[uint32]chan *hysteria.UDPMessage
|
||||||
@ -126,7 +127,7 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
if len(options.TLS.ALPN) == 0 {
|
if len(options.TLS.ALPN) == 0 {
|
||||||
options.TLS.ALPN = []string{hysteria.DefaultALPN}
|
options.TLS.ALPN = []string{hysteria.DefaultALPN}
|
||||||
}
|
}
|
||||||
tlsConfig, err := tls.NewServer(ctx, router, logger, common.PtrValueOrDefault(options.TLS))
|
tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -147,11 +148,7 @@ func (h *Hysteria) Start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rawConfig, err := h.tlsConfig.Config()
|
listener, err := qtls.Listen(packetConn, h.tlsConfig, h.quicConfig)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
listener, err := quic.Listen(packetConn, rawConfig, h.quicConfig)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -333,7 +330,7 @@ func (h *Hysteria) Close() error {
|
|||||||
h.udpAccess.Unlock()
|
h.udpAccess.Unlock()
|
||||||
return common.Close(
|
return common.Close(
|
||||||
&h.myInboundAdapter,
|
&h.myInboundAdapter,
|
||||||
common.PtrOrNil(h.listener),
|
h.listener,
|
||||||
h.tlsConfig,
|
h.tlsConfig,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
144
inbound/hysteria2.go
Normal file
144
inbound/hysteria2.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
//go:build with_quic
|
||||||
|
|
||||||
|
package inbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing-box/transport/hysteria2"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/auth"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ adapter.Inbound = (*Hysteria2)(nil)
|
||||||
|
|
||||||
|
type Hysteria2 struct {
|
||||||
|
myInboundAdapter
|
||||||
|
tlsConfig tls.ServerConfig
|
||||||
|
server *hysteria2.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHysteria2(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2InboundOptions) (*Hysteria2, error) {
|
||||||
|
if options.TLS == nil || !options.TLS.Enabled {
|
||||||
|
return nil, C.ErrTLSRequired
|
||||||
|
}
|
||||||
|
tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var salamanderPassword string
|
||||||
|
if options.Obfs != nil {
|
||||||
|
if options.Obfs.Password == "" {
|
||||||
|
return nil, E.New("missing obfs password")
|
||||||
|
}
|
||||||
|
switch options.Obfs.Type {
|
||||||
|
case hysteria2.ObfsTypeSalamander:
|
||||||
|
salamanderPassword = options.Obfs.Password
|
||||||
|
default:
|
||||||
|
return nil, E.New("unknown obfs type: ", options.Obfs.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var masqueradeHandler http.Handler
|
||||||
|
if options.Masquerade != "" {
|
||||||
|
masqueradeURL, err := url.Parse(options.Masquerade)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "parse masquerade URL")
|
||||||
|
}
|
||||||
|
switch masqueradeURL.Scheme {
|
||||||
|
case "file":
|
||||||
|
masqueradeHandler = http.FileServer(http.Dir(masqueradeURL.Path))
|
||||||
|
case "http", "https":
|
||||||
|
masqueradeHandler = &httputil.ReverseProxy{
|
||||||
|
Rewrite: func(r *httputil.ProxyRequest) {
|
||||||
|
r.SetURL(masqueradeURL)
|
||||||
|
r.Out.Host = r.In.Host
|
||||||
|
},
|
||||||
|
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
|
w.WriteHeader(http.StatusBadGateway)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, E.New("unknown masquerade URL scheme: ", masqueradeURL.Scheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inbound := &Hysteria2{
|
||||||
|
myInboundAdapter: myInboundAdapter{
|
||||||
|
protocol: C.TypeHysteria2,
|
||||||
|
network: []string{N.NetworkUDP},
|
||||||
|
ctx: ctx,
|
||||||
|
router: router,
|
||||||
|
logger: logger,
|
||||||
|
tag: tag,
|
||||||
|
listenOptions: options.ListenOptions,
|
||||||
|
},
|
||||||
|
tlsConfig: tlsConfig,
|
||||||
|
}
|
||||||
|
server, err := hysteria2.NewServer(hysteria2.ServerOptions{
|
||||||
|
Context: ctx,
|
||||||
|
Logger: logger,
|
||||||
|
SendBPS: uint64(options.UpMbps * 1024 * 1024),
|
||||||
|
ReceiveBPS: uint64(options.DownMbps * 1024 * 1024),
|
||||||
|
SalamanderPassword: salamanderPassword,
|
||||||
|
TLSConfig: tlsConfig,
|
||||||
|
Users: common.Map(options.Users, func(it option.Hysteria2User) hysteria2.User {
|
||||||
|
return hysteria2.User(it)
|
||||||
|
}),
|
||||||
|
IgnoreClientBandwidth: options.IgnoreClientBandwidth,
|
||||||
|
Handler: adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, nil),
|
||||||
|
MasqueradeHandler: masqueradeHandler,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
inbound.server = server
|
||||||
|
return inbound, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria2) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
||||||
|
ctx = log.ContextWithNewID(ctx)
|
||||||
|
h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
|
||||||
|
metadata = h.createMetadata(conn, metadata)
|
||||||
|
metadata.User, _ = auth.UserFromContext[string](ctx)
|
||||||
|
return h.router.RouteConnection(ctx, conn, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria2) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
|
||||||
|
ctx = log.ContextWithNewID(ctx)
|
||||||
|
metadata = h.createPacketMetadata(conn, metadata)
|
||||||
|
metadata.User, _ = auth.UserFromContext[string](ctx)
|
||||||
|
h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
|
||||||
|
return h.router.RoutePacketConnection(ctx, conn, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria2) Start() error {
|
||||||
|
if h.tlsConfig != nil {
|
||||||
|
err := h.tlsConfig.Start()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
packetConn, err := h.myInboundAdapter.ListenUDP()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return h.server.Start(packetConn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria2) Close() error {
|
||||||
|
return common.Close(
|
||||||
|
&h.myInboundAdapter,
|
||||||
|
h.tlsConfig,
|
||||||
|
common.PtrOrNil(h.server),
|
||||||
|
)
|
||||||
|
}
|
@ -14,3 +14,7 @@ import (
|
|||||||
func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaInboundOptions) (adapter.Inbound, error) {
|
func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaInboundOptions) (adapter.Inbound, error) {
|
||||||
return nil, C.ErrQUICNotIncluded
|
return nil, C.ErrQUICNotIncluded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewHysteria2(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2InboundOptions) (adapter.Inbound, error) {
|
||||||
|
return nil, C.ErrQUICNotIncluded
|
||||||
|
}
|
||||||
|
@ -60,7 +60,7 @@ func NewNaive(ctx context.Context, router adapter.Router, logger log.ContextLogg
|
|||||||
return nil, E.New("missing users")
|
return nil, E.New("missing users")
|
||||||
}
|
}
|
||||||
if options.TLS != nil {
|
if options.TLS != nil {
|
||||||
tlsConfig, err := tls.NewServer(ctx, router, logger, common.PtrValueOrDefault(options.TLS))
|
tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -3,28 +3,39 @@
|
|||||||
package inbound
|
package inbound
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/sagernet/quic-go"
|
||||||
"github.com/sagernet/quic-go/http3"
|
"github.com/sagernet/quic-go/http3"
|
||||||
|
"github.com/sagernet/sing-box/common/qtls"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (n *Naive) configureHTTP3Listener() error {
|
func (n *Naive) configureHTTP3Listener() error {
|
||||||
tlsConfig, err := n.tlsConfig.Config()
|
err := qtls.ConfigureHTTP3(n.tlsConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
h3Server := &http3.Server{
|
|
||||||
Port: int(n.listenOptions.ListenPort),
|
|
||||||
TLSConfig: tlsConfig,
|
|
||||||
Handler: n,
|
|
||||||
}
|
|
||||||
|
|
||||||
udpConn, err := n.ListenUDP()
|
udpConn, err := n.ListenUDP()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
quicListener, err := qtls.ListenEarly(udpConn, n.tlsConfig, &quic.Config{
|
||||||
|
MaxIncomingStreams: 1 << 60,
|
||||||
|
Allow0RTT: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
udpConn.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
h3Server := &http3.Server{
|
||||||
|
Port: int(n.listenOptions.ListenPort),
|
||||||
|
Handler: n,
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
sErr := h3Server.Serve(udpConn)
|
sErr := h3Server.ServeListener(quicListener)
|
||||||
udpConn.Close()
|
udpConn.Close()
|
||||||
if sErr != nil && !E.IsClosedOrCanceled(sErr) {
|
if sErr != nil && !E.IsClosedOrCanceled(sErr) {
|
||||||
n.logger.Error("http3 server serve error: ", sErr)
|
n.logger.Error("http3 server serve error: ", sErr)
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/sagernet/sing/common/buf"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/common/ntp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) {
|
func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) {
|
||||||
@ -68,7 +69,7 @@ func newShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
|
|||||||
case common.Contains(shadowaead.List, options.Method):
|
case common.Contains(shadowaead.List, options.Method):
|
||||||
inbound.service, err = shadowaead.NewService(options.Method, nil, options.Password, udpTimeout, inbound.upstreamContextHandler())
|
inbound.service, err = shadowaead.NewService(options.Method, nil, options.Password, udpTimeout, inbound.upstreamContextHandler())
|
||||||
case common.Contains(shadowaead_2022.List, options.Method):
|
case common.Contains(shadowaead_2022.List, options.Method):
|
||||||
inbound.service, err = shadowaead_2022.NewServiceWithPassword(options.Method, options.Password, udpTimeout, inbound.upstreamContextHandler(), router.TimeFunc())
|
inbound.service, err = shadowaead_2022.NewServiceWithPassword(options.Method, options.Password, udpTimeout, inbound.upstreamContextHandler(), ntp.TimeFuncFromContext(ctx))
|
||||||
default:
|
default:
|
||||||
err = E.New("unsupported method: ", options.Method)
|
err = E.New("unsupported method: ", options.Method)
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
F "github.com/sagernet/sing/common/format"
|
F "github.com/sagernet/sing/common/format"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/common/ntp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -61,7 +62,7 @@ func newShadowsocksMulti(ctx context.Context, router adapter.Router, logger log.
|
|||||||
options.Password,
|
options.Password,
|
||||||
udpTimeout,
|
udpTimeout,
|
||||||
adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound),
|
adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound),
|
||||||
router.TimeFunc(),
|
ntp.TimeFuncFromContext(ctx),
|
||||||
)
|
)
|
||||||
} else if common.Contains(shadowaead.List, options.Method) {
|
} else if common.Contains(shadowaead.List, options.Method) {
|
||||||
service, err = shadowaead.NewMultiService[int](
|
service, err = shadowaead.NewMultiService[int](
|
||||||
|
@ -49,7 +49,7 @@ func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLog
|
|||||||
users: options.Users,
|
users: options.Users,
|
||||||
}
|
}
|
||||||
if options.TLS != nil {
|
if options.TLS != nil {
|
||||||
tlsConfig, err := tls.NewServer(ctx, router, logger, common.PtrValueOrDefault(options.TLS))
|
tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -34,11 +34,7 @@ func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogge
|
|||||||
if options.TLS == nil || !options.TLS.Enabled {
|
if options.TLS == nil || !options.TLS.Enabled {
|
||||||
return nil, C.ErrTLSRequired
|
return nil, C.ErrTLSRequired
|
||||||
}
|
}
|
||||||
tlsConfig, err := tls.NewServer(ctx, router, logger, common.PtrValueOrDefault(options.TLS))
|
tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rawConfig, err := tlsConfig.Config()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -67,7 +63,7 @@ func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogge
|
|||||||
server, err := tuic.NewServer(tuic.ServerOptions{
|
server, err := tuic.NewServer(tuic.ServerOptions{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
TLSConfig: rawConfig,
|
TLSConfig: tlsConfig,
|
||||||
Users: users,
|
Users: users,
|
||||||
CongestionControl: options.CongestionControl,
|
CongestionControl: options.CongestionControl,
|
||||||
AuthTimeout: time.Duration(options.AuthTimeout),
|
AuthTimeout: time.Duration(options.AuthTimeout),
|
||||||
@ -115,6 +111,7 @@ func (h *TUIC) Start() error {
|
|||||||
func (h *TUIC) Close() error {
|
func (h *TUIC) Close() error {
|
||||||
return common.Close(
|
return common.Close(
|
||||||
&h.myInboundAdapter,
|
&h.myInboundAdapter,
|
||||||
|
h.tlsConfig,
|
||||||
common.PtrOrNil(h.server),
|
common.PtrOrNil(h.server),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,7 @@ func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogg
|
|||||||
inbound.service = service
|
inbound.service = service
|
||||||
var err error
|
var err error
|
||||||
if options.TLS != nil {
|
if options.TLS != nil {
|
||||||
inbound.tlsConfig, err = tls.NewServer(ctx, router, logger, common.PtrValueOrDefault(options.TLS))
|
inbound.tlsConfig, err = tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ import (
|
|||||||
F "github.com/sagernet/sing/common/format"
|
F "github.com/sagernet/sing/common/format"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/common/ntp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -50,7 +51,7 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg
|
|||||||
users: options.Users,
|
users: options.Users,
|
||||||
}
|
}
|
||||||
var serviceOptions []vmess.ServiceOption
|
var serviceOptions []vmess.ServiceOption
|
||||||
if timeFunc := router.TimeFunc(); timeFunc != nil {
|
if timeFunc := ntp.TimeFuncFromContext(ctx); timeFunc != nil {
|
||||||
serviceOptions = append(serviceOptions, vmess.ServiceWithTimeFunc(timeFunc))
|
serviceOptions = append(serviceOptions, vmess.ServiceWithTimeFunc(timeFunc))
|
||||||
}
|
}
|
||||||
if options.Transport != nil && options.Transport.Type != "" {
|
if options.Transport != nil && options.Transport.Type != "" {
|
||||||
@ -69,7 +70,7 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if options.TLS != nil {
|
if options.TLS != nil {
|
||||||
inbound.tlsConfig, err = tls.NewServer(ctx, router, logger, common.PtrValueOrDefault(options.TLS))
|
inbound.tlsConfig, err = tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -87,6 +87,7 @@ nav:
|
|||||||
- ShadowTLS: configuration/inbound/shadowtls.md
|
- ShadowTLS: configuration/inbound/shadowtls.md
|
||||||
- VLESS: configuration/inbound/vless.md
|
- VLESS: configuration/inbound/vless.md
|
||||||
- TUIC: configuration/inbound/tuic.md
|
- TUIC: configuration/inbound/tuic.md
|
||||||
|
- Hysteria2: configuration/inbound/hysteria2.md
|
||||||
- Tun: configuration/inbound/tun.md
|
- Tun: configuration/inbound/tun.md
|
||||||
- Redirect: configuration/inbound/redirect.md
|
- Redirect: configuration/inbound/redirect.md
|
||||||
- TProxy: configuration/inbound/tproxy.md
|
- TProxy: configuration/inbound/tproxy.md
|
||||||
@ -105,6 +106,7 @@ nav:
|
|||||||
- ShadowsocksR: configuration/outbound/shadowsocksr.md
|
- ShadowsocksR: configuration/outbound/shadowsocksr.md
|
||||||
- VLESS: configuration/outbound/vless.md
|
- VLESS: configuration/outbound/vless.md
|
||||||
- TUIC: configuration/outbound/tuic.md
|
- TUIC: configuration/outbound/tuic.md
|
||||||
|
- Hysteria2: configuration/outbound/hysteria2.md
|
||||||
- Tor: configuration/outbound/tor.md
|
- Tor: configuration/outbound/tor.md
|
||||||
- SSH: configuration/outbound/ssh.md
|
- SSH: configuration/outbound/ssh.md
|
||||||
- DNS: configuration/outbound/dns.md
|
- DNS: configuration/outbound/dns.md
|
||||||
|
@ -18,7 +18,7 @@ import (
|
|||||||
"github.com/sagernet/sing/common/ntp"
|
"github.com/sagernet/sing/common/ntp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ adapter.TimeService = (*Service)(nil)
|
var _ ntp.TimeService = (*Service)(nil)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
33
option/hysteria2.go
Normal file
33
option/hysteria2.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package option
|
||||||
|
|
||||||
|
type Hysteria2InboundOptions struct {
|
||||||
|
ListenOptions
|
||||||
|
UpMbps int `json:"up_mbps,omitempty"`
|
||||||
|
DownMbps int `json:"down_mbps,omitempty"`
|
||||||
|
Obfs *Hysteria2Obfs `json:"obfs,omitempty"`
|
||||||
|
Users []Hysteria2User `json:"users,omitempty"`
|
||||||
|
IgnoreClientBandwidth bool `json:"ignore_client_bandwidth,omitempty"`
|
||||||
|
TLS *InboundTLSOptions `json:"tls,omitempty"`
|
||||||
|
Masquerade string `json:"masquerade,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Hysteria2Obfs struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Hysteria2User struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Hysteria2OutboundOptions struct {
|
||||||
|
DialerOptions
|
||||||
|
ServerOptions
|
||||||
|
UpMbps int `json:"up_mbps,omitempty"`
|
||||||
|
DownMbps int `json:"down_mbps,omitempty"`
|
||||||
|
Obfs *Hysteria2Obfs `json:"obfs,omitempty"`
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
|
Network NetworkList `json:"network,omitempty"`
|
||||||
|
TLS *OutboundTLSOptions `json:"tls,omitempty"`
|
||||||
|
}
|
@ -24,6 +24,7 @@ type _Inbound struct {
|
|||||||
ShadowTLSOptions ShadowTLSInboundOptions `json:"-"`
|
ShadowTLSOptions ShadowTLSInboundOptions `json:"-"`
|
||||||
VLESSOptions VLESSInboundOptions `json:"-"`
|
VLESSOptions VLESSInboundOptions `json:"-"`
|
||||||
TUICOptions TUICInboundOptions `json:"-"`
|
TUICOptions TUICInboundOptions `json:"-"`
|
||||||
|
Hysteria2Options Hysteria2InboundOptions `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Inbound _Inbound
|
type Inbound _Inbound
|
||||||
@ -61,6 +62,8 @@ func (h Inbound) MarshalJSON() ([]byte, error) {
|
|||||||
v = h.VLESSOptions
|
v = h.VLESSOptions
|
||||||
case C.TypeTUIC:
|
case C.TypeTUIC:
|
||||||
v = h.TUICOptions
|
v = h.TUICOptions
|
||||||
|
case C.TypeHysteria2:
|
||||||
|
v = h.Hysteria2Options
|
||||||
default:
|
default:
|
||||||
return nil, E.New("unknown inbound type: ", h.Type)
|
return nil, E.New("unknown inbound type: ", h.Type)
|
||||||
}
|
}
|
||||||
@ -104,6 +107,8 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error {
|
|||||||
v = &h.VLESSOptions
|
v = &h.VLESSOptions
|
||||||
case C.TypeTUIC:
|
case C.TypeTUIC:
|
||||||
v = &h.TUICOptions
|
v = &h.TUICOptions
|
||||||
|
case C.TypeHysteria2:
|
||||||
|
v = &h.Hysteria2Options
|
||||||
default:
|
default:
|
||||||
return E.New("unknown inbound type: ", h.Type)
|
return E.New("unknown inbound type: ", h.Type)
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ type _Outbound struct {
|
|||||||
ShadowsocksROptions ShadowsocksROutboundOptions `json:"-"`
|
ShadowsocksROptions ShadowsocksROutboundOptions `json:"-"`
|
||||||
VLESSOptions VLESSOutboundOptions `json:"-"`
|
VLESSOptions VLESSOutboundOptions `json:"-"`
|
||||||
TUICOptions TUICOutboundOptions `json:"-"`
|
TUICOptions TUICOutboundOptions `json:"-"`
|
||||||
|
Hysteria2Options Hysteria2OutboundOptions `json:"-"`
|
||||||
SelectorOptions SelectorOutboundOptions `json:"-"`
|
SelectorOptions SelectorOutboundOptions `json:"-"`
|
||||||
URLTestOptions URLTestOutboundOptions `json:"-"`
|
URLTestOptions URLTestOutboundOptions `json:"-"`
|
||||||
}
|
}
|
||||||
@ -63,6 +64,8 @@ func (h Outbound) MarshalJSON() ([]byte, error) {
|
|||||||
v = h.VLESSOptions
|
v = h.VLESSOptions
|
||||||
case C.TypeTUIC:
|
case C.TypeTUIC:
|
||||||
v = h.TUICOptions
|
v = h.TUICOptions
|
||||||
|
case C.TypeHysteria2:
|
||||||
|
v = h.Hysteria2Options
|
||||||
case C.TypeSelector:
|
case C.TypeSelector:
|
||||||
v = h.SelectorOptions
|
v = h.SelectorOptions
|
||||||
case C.TypeURLTest:
|
case C.TypeURLTest:
|
||||||
@ -110,6 +113,8 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error {
|
|||||||
v = &h.VLESSOptions
|
v = &h.VLESSOptions
|
||||||
case C.TypeTUIC:
|
case C.TypeTUIC:
|
||||||
v = &h.TUICOptions
|
v = &h.TUICOptions
|
||||||
|
case C.TypeHysteria2:
|
||||||
|
v = &h.Hysteria2Options
|
||||||
case C.TypeSelector:
|
case C.TypeSelector:
|
||||||
v = &h.SelectorOptions
|
v = &h.SelectorOptions
|
||||||
case C.TypeURLTest:
|
case C.TypeURLTest:
|
||||||
|
@ -8,11 +8,12 @@ type InboundTLSOptions struct {
|
|||||||
MinVersion string `json:"min_version,omitempty"`
|
MinVersion string `json:"min_version,omitempty"`
|
||||||
MaxVersion string `json:"max_version,omitempty"`
|
MaxVersion string `json:"max_version,omitempty"`
|
||||||
CipherSuites Listable[string] `json:"cipher_suites,omitempty"`
|
CipherSuites Listable[string] `json:"cipher_suites,omitempty"`
|
||||||
Certificate string `json:"certificate,omitempty"`
|
Certificate Listable[string] `json:"certificate,omitempty"`
|
||||||
CertificatePath string `json:"certificate_path,omitempty"`
|
CertificatePath string `json:"certificate_path,omitempty"`
|
||||||
Key string `json:"key,omitempty"`
|
Key Listable[string] `json:"key,omitempty"`
|
||||||
KeyPath string `json:"key_path,omitempty"`
|
KeyPath string `json:"key_path,omitempty"`
|
||||||
ACME *InboundACMEOptions `json:"acme,omitempty"`
|
ACME *InboundACMEOptions `json:"acme,omitempty"`
|
||||||
|
ECH *InboundECHOptions `json:"ech,omitempty"`
|
||||||
Reality *InboundRealityOptions `json:"reality,omitempty"`
|
Reality *InboundRealityOptions `json:"reality,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,11 +46,20 @@ type InboundRealityHandshakeOptions struct {
|
|||||||
DialerOptions
|
DialerOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InboundECHOptions struct {
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
PQSignatureSchemesEnabled bool `json:"pq_signature_schemes_enabled,omitempty"`
|
||||||
|
DynamicRecordSizingDisabled bool `json:"dynamic_record_sizing_disabled,omitempty"`
|
||||||
|
Key Listable[string] `json:"key,omitempty"`
|
||||||
|
KeyPath string `json:"key_path,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type OutboundECHOptions struct {
|
type OutboundECHOptions struct {
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
PQSignatureSchemesEnabled bool `json:"pq_signature_schemes_enabled,omitempty"`
|
PQSignatureSchemesEnabled bool `json:"pq_signature_schemes_enabled,omitempty"`
|
||||||
DynamicRecordSizingDisabled bool `json:"dynamic_record_sizing_disabled,omitempty"`
|
DynamicRecordSizingDisabled bool `json:"dynamic_record_sizing_disabled,omitempty"`
|
||||||
Config string `json:"config,omitempty"`
|
Config Listable[string] `json:"config,omitempty"`
|
||||||
|
ConfigPath string `json:"config_path,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OutboundUTLSOptions struct {
|
type OutboundUTLSOptions struct {
|
||||||
|
@ -30,7 +30,7 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, t
|
|||||||
case C.TypeSOCKS:
|
case C.TypeSOCKS:
|
||||||
return NewSocks(router, logger, tag, options.SocksOptions)
|
return NewSocks(router, logger, tag, options.SocksOptions)
|
||||||
case C.TypeHTTP:
|
case C.TypeHTTP:
|
||||||
return NewHTTP(router, logger, tag, options.HTTPOptions)
|
return NewHTTP(ctx, router, logger, tag, options.HTTPOptions)
|
||||||
case C.TypeShadowsocks:
|
case C.TypeShadowsocks:
|
||||||
return NewShadowsocks(ctx, router, logger, tag, options.ShadowsocksOptions)
|
return NewShadowsocks(ctx, router, logger, tag, options.ShadowsocksOptions)
|
||||||
case C.TypeVMess:
|
case C.TypeVMess:
|
||||||
@ -53,6 +53,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, t
|
|||||||
return NewVLESS(ctx, router, logger, tag, options.VLESSOptions)
|
return NewVLESS(ctx, router, logger, tag, options.VLESSOptions)
|
||||||
case C.TypeTUIC:
|
case C.TypeTUIC:
|
||||||
return NewTUIC(ctx, router, logger, tag, options.TUICOptions)
|
return NewTUIC(ctx, router, logger, tag, options.TUICOptions)
|
||||||
|
case C.TypeHysteria2:
|
||||||
|
return NewHysteria2(ctx, router, logger, tag, options.Hysteria2Options)
|
||||||
case C.TypeSelector:
|
case C.TypeSelector:
|
||||||
return NewSelector(router, logger, tag, options.SelectorOptions)
|
return NewSelector(router, logger, tag, options.SelectorOptions)
|
||||||
case C.TypeURLTest:
|
case C.TypeURLTest:
|
||||||
|
@ -25,12 +25,12 @@ type HTTP struct {
|
|||||||
client *sHTTP.Client
|
client *sHTTP.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTP(router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPOutboundOptions) (*HTTP, error) {
|
func NewHTTP(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPOutboundOptions) (*HTTP, error) {
|
||||||
outboundDialer, err := dialer.New(router, options.DialerOptions)
|
outboundDialer, err := dialer.New(router, options.DialerOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
detour, err := tls.NewDialerFromOptions(router, outboundDialer, options.Server, common.PtrValueOrDefault(options.TLS))
|
detour, err := tls.NewDialerFromOptions(ctx, router, outboundDialer, options.Server, common.PtrValueOrDefault(options.TLS))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/sagernet/quic-go/congestion"
|
"github.com/sagernet/quic-go/congestion"
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/dialer"
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
|
"github.com/sagernet/sing-box/common/qtls"
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
@ -33,7 +34,7 @@ type Hysteria struct {
|
|||||||
ctx context.Context
|
ctx context.Context
|
||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
serverAddr M.Socksaddr
|
serverAddr M.Socksaddr
|
||||||
tlsConfig *tls.STDConfig
|
tlsConfig tls.Config
|
||||||
quicConfig *quic.Config
|
quicConfig *quic.Config
|
||||||
authKey []byte
|
authKey []byte
|
||||||
xplusKey []byte
|
xplusKey []byte
|
||||||
@ -52,17 +53,12 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
if options.TLS == nil || !options.TLS.Enabled {
|
if options.TLS == nil || !options.TLS.Enabled {
|
||||||
return nil, C.ErrTLSRequired
|
return nil, C.ErrTLSRequired
|
||||||
}
|
}
|
||||||
abstractTLSConfig, err := tls.NewClient(router, options.Server, common.PtrValueOrDefault(options.TLS))
|
tlsConfig, err := tls.NewClient(ctx, options.Server, common.PtrValueOrDefault(options.TLS))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tlsConfig, err := abstractTLSConfig.Config()
|
if len(tlsConfig.NextProtos()) == 0 {
|
||||||
if err != nil {
|
tlsConfig.SetNextProtos([]string{hysteria.DefaultALPN})
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tlsConfig.MinVersion = tls.VersionTLS13
|
|
||||||
if len(tlsConfig.NextProtos) == 0 {
|
|
||||||
tlsConfig.NextProtos = []string{hysteria.DefaultALPN}
|
|
||||||
}
|
}
|
||||||
quicConfig := &quic.Config{
|
quicConfig := &quic.Config{
|
||||||
InitialStreamReceiveWindow: options.ReceiveWindowConn,
|
InitialStreamReceiveWindow: options.ReceiveWindowConn,
|
||||||
@ -182,7 +178,7 @@ func (h *Hysteria) offerNew(ctx context.Context) (quic.Connection, error) {
|
|||||||
packetConn = hysteria.NewXPlusPacketConn(packetConn, h.xplusKey)
|
packetConn = hysteria.NewXPlusPacketConn(packetConn, h.xplusKey)
|
||||||
}
|
}
|
||||||
packetConn = &hysteria.PacketConnWrapper{PacketConn: packetConn}
|
packetConn = &hysteria.PacketConnWrapper{PacketConn: packetConn}
|
||||||
quicConn, err := quic.Dial(h.ctx, packetConn, udpConn.RemoteAddr(), h.tlsConfig, h.quicConfig)
|
quicConn, err := qtls.Dial(h.ctx, packetConn, udpConn.RemoteAddr(), h.tlsConfig, h.quicConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
packetConn.Close()
|
packetConn.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
|
122
outbound/hysteria2.go
Normal file
122
outbound/hysteria2.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
//go:build with_quic
|
||||||
|
|
||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing-box/transport/hysteria2"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ adapter.Outbound = (*TUIC)(nil)
|
||||||
|
_ adapter.InterfaceUpdateListener = (*TUIC)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Hysteria2 struct {
|
||||||
|
myOutboundAdapter
|
||||||
|
client *hysteria2.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHysteria2(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2OutboundOptions) (*Hysteria2, error) {
|
||||||
|
options.UDPFragmentDefault = true
|
||||||
|
if options.TLS == nil || !options.TLS.Enabled {
|
||||||
|
return nil, C.ErrTLSRequired
|
||||||
|
}
|
||||||
|
tlsConfig, err := tls.NewClient(ctx, options.Server, common.PtrValueOrDefault(options.TLS))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var salamanderPassword string
|
||||||
|
if options.Obfs != nil {
|
||||||
|
if options.Obfs.Password == "" {
|
||||||
|
return nil, E.New("missing obfs password")
|
||||||
|
}
|
||||||
|
switch options.Obfs.Type {
|
||||||
|
case hysteria2.ObfsTypeSalamander:
|
||||||
|
salamanderPassword = options.Obfs.Password
|
||||||
|
default:
|
||||||
|
return nil, E.New("unknown obfs type: ", options.Obfs.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outboundDialer, err := dialer.New(router, options.DialerOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
networkList := options.Network.Build()
|
||||||
|
client, err := hysteria2.NewClient(hysteria2.ClientOptions{
|
||||||
|
Context: ctx,
|
||||||
|
Dialer: outboundDialer,
|
||||||
|
ServerAddress: options.ServerOptions.Build(),
|
||||||
|
SendBPS: uint64(options.UpMbps * 1024 * 1024),
|
||||||
|
ReceiveBPS: uint64(options.DownMbps * 1024 * 1024),
|
||||||
|
SalamanderPassword: salamanderPassword,
|
||||||
|
Password: options.Password,
|
||||||
|
TLSConfig: tlsConfig,
|
||||||
|
UDPDisabled: !common.Contains(networkList, N.NetworkUDP),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Hysteria2{
|
||||||
|
myOutboundAdapter: myOutboundAdapter{
|
||||||
|
protocol: C.TypeHysteria2,
|
||||||
|
network: networkList,
|
||||||
|
router: router,
|
||||||
|
logger: logger,
|
||||||
|
tag: tag,
|
||||||
|
dependencies: withDialerDependency(options.DialerOptions),
|
||||||
|
},
|
||||||
|
client: client,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria2) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
|
switch N.NetworkName(network) {
|
||||||
|
case N.NetworkTCP:
|
||||||
|
h.logger.InfoContext(ctx, "outbound connection to ", destination)
|
||||||
|
return h.client.DialConn(ctx, destination)
|
||||||
|
case N.NetworkUDP:
|
||||||
|
conn, err := h.ListenPacket(ctx, destination)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return bufio.NewBindPacketConn(conn, destination), nil
|
||||||
|
default:
|
||||||
|
return nil, E.New("unsupported network: ", network)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria2) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
|
h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
|
||||||
|
return h.client.ListenPacket(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria2) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
||||||
|
return NewConnection(ctx, h, conn, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria2) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
|
||||||
|
return NewPacketConnection(ctx, h, conn, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria2) InterfaceUpdated() error {
|
||||||
|
return h.client.CloseWithError(E.New("network changed"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hysteria2) Close() error {
|
||||||
|
return h.client.CloseWithError(os.ErrClosed)
|
||||||
|
}
|
@ -14,3 +14,7 @@ import (
|
|||||||
func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaOutboundOptions) (adapter.Outbound, error) {
|
func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaOutboundOptions) (adapter.Outbound, error) {
|
||||||
return nil, C.ErrQUICNotIncluded
|
return nil, C.ErrQUICNotIncluded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewHysteria2(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2OutboundOptions) (adapter.Outbound, error) {
|
||||||
|
return nil, C.ErrQUICNotIncluded
|
||||||
|
}
|
||||||
|
@ -57,7 +57,7 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
|
|||||||
serverAddr: options.ServerOptions.Build(),
|
serverAddr: options.ServerOptions.Build(),
|
||||||
}
|
}
|
||||||
if options.Plugin != "" {
|
if options.Plugin != "" {
|
||||||
outbound.plugin, err = sip003.CreatePlugin(options.Plugin, options.PluginOptions, router, outbound.dialer, outbound.serverAddr)
|
outbound.plugin, err = sip003.CreatePlugin(ctx, options.Plugin, options.PluginOptions, router, outbound.dialer, outbound.serverAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.Context
|
|||||||
options.TLS.MinVersion = "1.2"
|
options.TLS.MinVersion = "1.2"
|
||||||
options.TLS.MaxVersion = "1.2"
|
options.TLS.MaxVersion = "1.2"
|
||||||
}
|
}
|
||||||
tlsConfig, err := tls.NewClient(router, options.Server, common.PtrValueOrDefault(options.TLS))
|
tlsConfig, err := tls.NewClient(ctx, options.Server, common.PtrValueOrDefault(options.TLS))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLog
|
|||||||
key: trojan.Key(options.Password),
|
key: trojan.Key(options.Password),
|
||||||
}
|
}
|
||||||
if options.TLS != nil {
|
if options.TLS != nil {
|
||||||
outbound.tlsConfig, err = tls.NewClient(router, options.Server, common.PtrValueOrDefault(options.TLS))
|
outbound.tlsConfig, err = tls.NewClient(ctx, options.Server, common.PtrValueOrDefault(options.TLS))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -41,11 +41,7 @@ func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogge
|
|||||||
if options.TLS == nil || !options.TLS.Enabled {
|
if options.TLS == nil || !options.TLS.Enabled {
|
||||||
return nil, C.ErrTLSRequired
|
return nil, C.ErrTLSRequired
|
||||||
}
|
}
|
||||||
abstractTLSConfig, err := tls.NewClient(router, options.Server, common.PtrValueOrDefault(options.TLS))
|
tlsConfig, err := tls.NewClient(ctx, options.Server, common.PtrValueOrDefault(options.TLS))
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tlsConfig, err := abstractTLSConfig.Config()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogg
|
|||||||
serverAddr: options.ServerOptions.Build(),
|
serverAddr: options.ServerOptions.Build(),
|
||||||
}
|
}
|
||||||
if options.TLS != nil {
|
if options.TLS != nil {
|
||||||
outbound.tlsConfig, err = tls.NewClient(router, options.Server, common.PtrValueOrDefault(options.TLS))
|
outbound.tlsConfig, err = tls.NewClient(ctx, options.Server, common.PtrValueOrDefault(options.TLS))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/common/ntp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ adapter.Outbound = (*VMess)(nil)
|
var _ adapter.Outbound = (*VMess)(nil)
|
||||||
@ -52,7 +53,7 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg
|
|||||||
serverAddr: options.ServerOptions.Build(),
|
serverAddr: options.ServerOptions.Build(),
|
||||||
}
|
}
|
||||||
if options.TLS != nil {
|
if options.TLS != nil {
|
||||||
outbound.tlsConfig, err = tls.NewClient(router, options.Server, common.PtrValueOrDefault(options.TLS))
|
outbound.tlsConfig, err = tls.NewClient(ctx, options.Server, common.PtrValueOrDefault(options.TLS))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -77,7 +78,7 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg
|
|||||||
return nil, E.New("unknown packet encoding: ", options.PacketEncoding)
|
return nil, E.New("unknown packet encoding: ", options.PacketEncoding)
|
||||||
}
|
}
|
||||||
var clientOptions []vmess.ClientOption
|
var clientOptions []vmess.ClientOption
|
||||||
if timeFunc := router.TimeFunc(); timeFunc != nil {
|
if timeFunc := ntp.TimeFuncFromContext(ctx); timeFunc != nil {
|
||||||
clientOptions = append(clientOptions, vmess.ClientWithTimeFunc(timeFunc))
|
clientOptions = append(clientOptions, vmess.ClientWithTimeFunc(timeFunc))
|
||||||
}
|
}
|
||||||
if options.GlobalPadding {
|
if options.GlobalPadding {
|
||||||
|
@ -81,7 +81,7 @@ type Router struct {
|
|||||||
interfaceMonitor tun.DefaultInterfaceMonitor
|
interfaceMonitor tun.DefaultInterfaceMonitor
|
||||||
packageManager tun.PackageManager
|
packageManager tun.PackageManager
|
||||||
processSearcher process.Searcher
|
processSearcher process.Searcher
|
||||||
timeService adapter.TimeService
|
timeService *ntp.Service
|
||||||
pauseManager pause.Manager
|
pauseManager pause.Manager
|
||||||
clashServer adapter.ClashServer
|
clashServer adapter.ClashServer
|
||||||
v2rayServer adapter.V2RayServer
|
v2rayServer adapter.V2RayServer
|
||||||
@ -950,13 +950,6 @@ func (r *Router) PackageManager() tun.PackageManager {
|
|||||||
return r.packageManager
|
return r.packageManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) TimeFunc() func() time.Time {
|
|
||||||
if r.timeService == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return r.timeService.TimeFunc()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) ClashServer() adapter.ClashServer {
|
func (r *Router) ClashServer() adapter.ClashServer {
|
||||||
return r.clashServer
|
return r.clashServer
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,8 @@ const (
|
|||||||
ImageTrojan = "trojangfw/trojan:latest"
|
ImageTrojan = "trojangfw/trojan:latest"
|
||||||
ImageNaive = "pocat/naiveproxy:client"
|
ImageNaive = "pocat/naiveproxy:client"
|
||||||
ImageBoringTun = "ghcr.io/ntkme/boringtun:edge"
|
ImageBoringTun = "ghcr.io/ntkme/boringtun:edge"
|
||||||
ImageHysteria = "tobyxdd/hysteria:latest"
|
ImageHysteria = "tobyxdd/hysteria:v1.3.5"
|
||||||
|
ImageHysteria2 = "tobyxdd/hysteria:v2"
|
||||||
ImageNginx = "nginx:stable"
|
ImageNginx = "nginx:stable"
|
||||||
ImageShadowTLS = "ghcr.io/ihciah/shadow-tls:latest"
|
ImageShadowTLS = "ghcr.io/ihciah/shadow-tls:latest"
|
||||||
ImageShadowsocksR = "teddysun/shadowsocks-r:latest"
|
ImageShadowsocksR = "teddysun/shadowsocks-r:latest"
|
||||||
@ -50,6 +51,7 @@ var allImages = []string{
|
|||||||
ImageNaive,
|
ImageNaive,
|
||||||
ImageBoringTun,
|
ImageBoringTun,
|
||||||
ImageHysteria,
|
ImageHysteria,
|
||||||
|
ImageHysteria2,
|
||||||
ImageNginx,
|
ImageNginx,
|
||||||
ImageShadowTLS,
|
ImageShadowTLS,
|
||||||
ImageShadowsocksR,
|
ImageShadowsocksR,
|
||||||
|
12
test/config/hysteria2-client.yml
Normal file
12
test/config/hysteria2-client.yml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
server: 127.0.0.1:10000
|
||||||
|
auth: password
|
||||||
|
fastOpen: true
|
||||||
|
socks5:
|
||||||
|
listen: 127.0.0.1:10001
|
||||||
|
tls:
|
||||||
|
sni: example.org
|
||||||
|
ca: /etc/hysteria/ca.pem
|
||||||
|
obfs:
|
||||||
|
type: salamander
|
||||||
|
salamander:
|
||||||
|
password: cry_me_a_r1ver
|
13
test/config/hysteria2-server.yml
Normal file
13
test/config/hysteria2-server.yml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
listen: 127.0.0.1:10000
|
||||||
|
auth:
|
||||||
|
type: password
|
||||||
|
password: password
|
||||||
|
fastOpen: true
|
||||||
|
tls:
|
||||||
|
sni: example.org
|
||||||
|
cert: /etc/hysteria/cert.pem
|
||||||
|
key: /etc/hysteria/key.pem
|
||||||
|
obfs:
|
||||||
|
type: salamander
|
||||||
|
salamander:
|
||||||
|
password: cry_me_a_r1ver
|
248
test/ech_test.go
Normal file
248
test/ech_test.go
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
|
||||||
|
"github.com/gofrs/uuid/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestECH(t *testing.T) {
|
||||||
|
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
||||||
|
echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org", false))
|
||||||
|
startInstance(t, option.Options{
|
||||||
|
Inbounds: []option.Inbound{
|
||||||
|
{
|
||||||
|
Type: C.TypeMixed,
|
||||||
|
Tag: "mixed-in",
|
||||||
|
MixedOptions: option.HTTPMixedInboundOptions{
|
||||||
|
ListenOptions: option.ListenOptions{
|
||||||
|
Listen: option.NewListenAddress(netip.IPv4Unspecified()),
|
||||||
|
ListenPort: clientPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.TypeTrojan,
|
||||||
|
TrojanOptions: option.TrojanInboundOptions{
|
||||||
|
ListenOptions: option.ListenOptions{
|
||||||
|
Listen: option.NewListenAddress(netip.IPv4Unspecified()),
|
||||||
|
ListenPort: serverPort,
|
||||||
|
},
|
||||||
|
Users: []option.TrojanUser{
|
||||||
|
{
|
||||||
|
Name: "sekai",
|
||||||
|
Password: "password",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TLS: &option.InboundTLSOptions{
|
||||||
|
Enabled: true,
|
||||||
|
ServerName: "example.org",
|
||||||
|
CertificatePath: certPem,
|
||||||
|
KeyPath: keyPem,
|
||||||
|
ECH: &option.InboundECHOptions{
|
||||||
|
Enabled: true,
|
||||||
|
Key: []string{echKey},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outbounds: []option.Outbound{
|
||||||
|
{
|
||||||
|
Type: C.TypeDirect,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.TypeTrojan,
|
||||||
|
Tag: "trojan-out",
|
||||||
|
TrojanOptions: option.TrojanOutboundOptions{
|
||||||
|
ServerOptions: option.ServerOptions{
|
||||||
|
Server: "127.0.0.1",
|
||||||
|
ServerPort: serverPort,
|
||||||
|
},
|
||||||
|
Password: "password",
|
||||||
|
TLS: &option.OutboundTLSOptions{
|
||||||
|
Enabled: true,
|
||||||
|
ServerName: "example.org",
|
||||||
|
CertificatePath: certPem,
|
||||||
|
ECH: &option.OutboundECHOptions{
|
||||||
|
Enabled: true,
|
||||||
|
Config: []string{echConfig},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Route: &option.RouteOptions{
|
||||||
|
Rules: []option.Rule{
|
||||||
|
{
|
||||||
|
DefaultOptions: option.DefaultRule{
|
||||||
|
Inbound: []string{"mixed-in"},
|
||||||
|
Outbound: "trojan-out",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
testSuit(t, clientPort, testPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestECHQUIC(t *testing.T) {
|
||||||
|
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
||||||
|
echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org", false))
|
||||||
|
startInstance(t, option.Options{
|
||||||
|
Inbounds: []option.Inbound{
|
||||||
|
{
|
||||||
|
Type: C.TypeMixed,
|
||||||
|
Tag: "mixed-in",
|
||||||
|
MixedOptions: option.HTTPMixedInboundOptions{
|
||||||
|
ListenOptions: option.ListenOptions{
|
||||||
|
Listen: option.NewListenAddress(netip.IPv4Unspecified()),
|
||||||
|
ListenPort: clientPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.TypeTUIC,
|
||||||
|
TUICOptions: option.TUICInboundOptions{
|
||||||
|
ListenOptions: option.ListenOptions{
|
||||||
|
Listen: option.NewListenAddress(netip.IPv4Unspecified()),
|
||||||
|
ListenPort: serverPort,
|
||||||
|
},
|
||||||
|
Users: []option.TUICUser{{
|
||||||
|
UUID: uuid.Nil.String(),
|
||||||
|
}},
|
||||||
|
TLS: &option.InboundTLSOptions{
|
||||||
|
Enabled: true,
|
||||||
|
ServerName: "example.org",
|
||||||
|
CertificatePath: certPem,
|
||||||
|
KeyPath: keyPem,
|
||||||
|
ECH: &option.InboundECHOptions{
|
||||||
|
Enabled: true,
|
||||||
|
Key: []string{echKey},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outbounds: []option.Outbound{
|
||||||
|
{
|
||||||
|
Type: C.TypeDirect,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.TypeTUIC,
|
||||||
|
Tag: "tuic-out",
|
||||||
|
TUICOptions: option.TUICOutboundOptions{
|
||||||
|
ServerOptions: option.ServerOptions{
|
||||||
|
Server: "127.0.0.1",
|
||||||
|
ServerPort: serverPort,
|
||||||
|
},
|
||||||
|
UUID: uuid.Nil.String(),
|
||||||
|
TLS: &option.OutboundTLSOptions{
|
||||||
|
Enabled: true,
|
||||||
|
ServerName: "example.org",
|
||||||
|
CertificatePath: certPem,
|
||||||
|
ECH: &option.OutboundECHOptions{
|
||||||
|
Enabled: true,
|
||||||
|
Config: []string{echConfig},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Route: &option.RouteOptions{
|
||||||
|
Rules: []option.Rule{
|
||||||
|
{
|
||||||
|
DefaultOptions: option.DefaultRule{
|
||||||
|
Inbound: []string{"mixed-in"},
|
||||||
|
Outbound: "tuic-out",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
testSuitLargeUDP(t, clientPort, testPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestECHHysteria2(t *testing.T) {
|
||||||
|
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
||||||
|
echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org", false))
|
||||||
|
startInstance(t, option.Options{
|
||||||
|
Inbounds: []option.Inbound{
|
||||||
|
{
|
||||||
|
Type: C.TypeMixed,
|
||||||
|
Tag: "mixed-in",
|
||||||
|
MixedOptions: option.HTTPMixedInboundOptions{
|
||||||
|
ListenOptions: option.ListenOptions{
|
||||||
|
Listen: option.NewListenAddress(netip.IPv4Unspecified()),
|
||||||
|
ListenPort: clientPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.TypeHysteria2,
|
||||||
|
Hysteria2Options: option.Hysteria2InboundOptions{
|
||||||
|
ListenOptions: option.ListenOptions{
|
||||||
|
Listen: option.NewListenAddress(netip.IPv4Unspecified()),
|
||||||
|
ListenPort: serverPort,
|
||||||
|
},
|
||||||
|
Users: []option.Hysteria2User{{
|
||||||
|
Password: "password",
|
||||||
|
}},
|
||||||
|
TLS: &option.InboundTLSOptions{
|
||||||
|
Enabled: true,
|
||||||
|
ServerName: "example.org",
|
||||||
|
CertificatePath: certPem,
|
||||||
|
KeyPath: keyPem,
|
||||||
|
ECH: &option.InboundECHOptions{
|
||||||
|
Enabled: true,
|
||||||
|
Key: []string{echKey},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outbounds: []option.Outbound{
|
||||||
|
{
|
||||||
|
Type: C.TypeDirect,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.TypeHysteria2,
|
||||||
|
Tag: "hy2-out",
|
||||||
|
Hysteria2Options: option.Hysteria2OutboundOptions{
|
||||||
|
ServerOptions: option.ServerOptions{
|
||||||
|
Server: "127.0.0.1",
|
||||||
|
ServerPort: serverPort,
|
||||||
|
},
|
||||||
|
Password: "password",
|
||||||
|
TLS: &option.OutboundTLSOptions{
|
||||||
|
Enabled: true,
|
||||||
|
ServerName: "example.org",
|
||||||
|
CertificatePath: certPem,
|
||||||
|
ECH: &option.OutboundECHOptions{
|
||||||
|
Enabled: true,
|
||||||
|
Config: []string{echConfig},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Route: &option.RouteOptions{
|
||||||
|
Rules: []option.Rule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultRule{
|
||||||
|
Inbound: []string{"mixed-in"},
|
||||||
|
Outbound: "hy2-out",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
testSuitLargeUDP(t, clientPort, testPort)
|
||||||
|
}
|
25
test/go.mod
25
test/go.mod
@ -16,7 +16,7 @@ require (
|
|||||||
github.com/spyzhov/ajson v0.9.0
|
github.com/spyzhov/ajson v0.9.0
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
go.uber.org/goleak v1.2.1
|
go.uber.org/goleak v1.2.1
|
||||||
golang.org/x/net v0.14.0
|
golang.org/x/net v0.15.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@ -27,7 +27,7 @@ require (
|
|||||||
github.com/ajg/form v1.5.1 // indirect
|
github.com/ajg/form v1.5.1 // indirect
|
||||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||||
github.com/caddyserver/certmagic v0.19.2 // indirect
|
github.com/caddyserver/certmagic v0.19.2 // indirect
|
||||||
github.com/cloudflare/circl v1.2.1-0.20221019164342-6ab4dfed8f3c // indirect
|
github.com/cloudflare/circl v1.3.3 // indirect
|
||||||
github.com/cretz/bine v0.2.0 // indirect
|
github.com/cretz/bine v0.2.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||||
@ -40,12 +40,11 @@ require (
|
|||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/mock v1.6.0 // indirect
|
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/google/btree v1.1.2 // indirect
|
github.com/google/btree v1.1.2 // indirect
|
||||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20230816195147-b3ca2534940d // indirect
|
github.com/insomniacslk/dhcp v0.0.0-20230906122924-c71a6be05968 // indirect
|
||||||
github.com/josharian/native v1.1.0 // indirect
|
github.com/josharian/native v1.1.0 // indirect
|
||||||
github.com/klauspost/compress v1.15.15 // indirect
|
github.com/klauspost/compress v1.15.15 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||||
@ -66,11 +65,11 @@ require (
|
|||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/quic-go/qpack v0.4.0 // indirect
|
github.com/quic-go/qpack v0.4.0 // indirect
|
||||||
github.com/quic-go/qtls-go1-20 v0.3.3 // indirect
|
github.com/quic-go/qtls-go1-20 v0.3.3 // indirect
|
||||||
github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0 // indirect
|
github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a // indirect
|
||||||
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 // indirect
|
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 // indirect
|
||||||
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2 // indirect
|
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2 // indirect
|
||||||
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect
|
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect
|
||||||
github.com/sagernet/quic-go v0.0.0-20230825040534-0cd917b2ddda // indirect
|
github.com/sagernet/quic-go v0.0.0-20230831052420-45809eee2e86 // indirect
|
||||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 // indirect
|
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 // indirect
|
||||||
github.com/sagernet/sing-dns v0.1.9-0.20230824120133-4d5cbceb40c1 // indirect
|
github.com/sagernet/sing-dns v0.1.9-0.20230824120133-4d5cbceb40c1 // indirect
|
||||||
github.com/sagernet/sing-mux v0.1.3-0.20230907005326-7befbadbf314 // indirect
|
github.com/sagernet/sing-mux v0.1.3-0.20230907005326-7befbadbf314 // indirect
|
||||||
@ -90,15 +89,15 @@ require (
|
|||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.25.0 // indirect
|
go.uber.org/zap v1.25.0 // indirect
|
||||||
go4.org/netipx v0.0.0-20230824141953-6213f710f925 // indirect
|
go4.org/netipx v0.0.0-20230824141953-6213f710f925 // indirect
|
||||||
golang.org/x/crypto v0.12.0 // indirect
|
golang.org/x/crypto v0.13.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
golang.org/x/mod v0.12.0 // indirect
|
golang.org/x/mod v0.12.0 // indirect
|
||||||
golang.org/x/sys v0.11.0 // indirect
|
golang.org/x/sys v0.12.0 // indirect
|
||||||
golang.org/x/text v0.12.0 // indirect
|
golang.org/x/text v0.13.0 // indirect
|
||||||
golang.org/x/time v0.3.0 // indirect
|
golang.org/x/time v0.3.0 // indirect
|
||||||
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect
|
golang.org/x/tools v0.13.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
|
||||||
google.golang.org/grpc v1.57.0 // indirect
|
google.golang.org/grpc v1.58.0 // indirect
|
||||||
google.golang.org/protobuf v1.31.0 // indirect
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
gotest.tools/v3 v3.4.0 // indirect
|
gotest.tools/v3 v3.4.0 // indirect
|
||||||
|
59
test/go.sum
59
test/go.sum
@ -17,8 +17,8 @@ github.com/caddyserver/certmagic v0.19.2/go.mod h1:fsL01NomQ6N+kE2j37ZCnig2MFosG
|
|||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
github.com/cloudflare/circl v1.2.1-0.20221019164342-6ab4dfed8f3c h1:K1VdSnBZiGapczwcUKnE1qcsMBclA84DUOD2NG/78VY=
|
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
|
||||||
github.com/cloudflare/circl v1.2.1-0.20221019164342-6ab4dfed8f3c/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw=
|
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||||
github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw=
|
github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw=
|
||||||
github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
|
github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
|
||||||
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
|
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
|
||||||
@ -52,8 +52,6 @@ github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
|
|||||||
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
|
||||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
@ -66,8 +64,8 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLe
|
|||||||
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||||
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20230816195147-b3ca2534940d h1:Ka64cclWedOkGzm9M2/XYuwJUdmWRUozmsxW0PyKA3A=
|
github.com/insomniacslk/dhcp v0.0.0-20230906122924-c71a6be05968 h1:uBiv5/8x42J7myumCdFuDOc5HnEXRK6eOtefwvE6+TQ=
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20230816195147-b3ca2534940d/go.mod h1:7474bZ1YNCvarT6WFKie4kEET6J0KYRDC4XJqqXzQW4=
|
github.com/insomniacslk/dhcp v0.0.0-20230906122924-c71a6be05968/go.mod h1:zmdm3sTSDP3vOOX3CEWRkkRHtKr1DxBx+J1OQFoDQQs=
|
||||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
@ -115,16 +113,16 @@ github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
|||||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||||
github.com/quic-go/qtls-go1-20 v0.3.3 h1:17/glZSLI9P9fDAeyCHBFSWSqJcwx1byhLwP5eUIDCM=
|
github.com/quic-go/qtls-go1-20 v0.3.3 h1:17/glZSLI9P9fDAeyCHBFSWSqJcwx1byhLwP5eUIDCM=
|
||||||
github.com/quic-go/qtls-go1-20 v0.3.3/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
github.com/quic-go/qtls-go1-20 v0.3.3/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||||
github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0 h1:KyhtFFt1Jtp5vW2ohNvstvQffTOQ/s5vENuGXzdA+TM=
|
github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a h1:wZHruBxZCsQLXHAozWpnJBL3wJ/XufDpz0qKtgpSnA4=
|
||||||
github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0/go.mod h1:D4SFEOkJK+4W1v86ZhX0jPM0rAL498fyQAChqMtes/I=
|
github.com/sagernet/cloudflare-tls v0.0.0-20230829051644-4a68352d0c4a/go.mod h1:dNV1ZP9y3qx5ltULeKaQZTZWTLHflgW5DES+Ses7cMI=
|
||||||
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA=
|
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA=
|
||||||
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h1:QUQ4RRHD6hGGHdFMEtR8T2P6GS6R3D/CXKdaYHKKXms=
|
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h1:QUQ4RRHD6hGGHdFMEtR8T2P6GS6R3D/CXKdaYHKKXms=
|
||||||
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2 h1:dnkKrzapqtAwjTSWt6hdPrARORfoYvuUczynvRLrueo=
|
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2 h1:dnkKrzapqtAwjTSWt6hdPrARORfoYvuUczynvRLrueo=
|
||||||
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2/go.mod h1:1JUiV7nGuf++YFm9eWZ8q2lrwHmhcUGzptMl/vL1+LA=
|
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2/go.mod h1:1JUiV7nGuf++YFm9eWZ8q2lrwHmhcUGzptMl/vL1+LA=
|
||||||
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
|
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
|
||||||
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
||||||
github.com/sagernet/quic-go v0.0.0-20230825040534-0cd917b2ddda h1:7J/hnOFqCThiCrVpvr0wKO+Dic/XPSulPr5yI8FVJMs=
|
github.com/sagernet/quic-go v0.0.0-20230831052420-45809eee2e86 h1:g4TEg9inAtA1FDTXpNrvmx72nN5mTOLQrJce6fVxF9g=
|
||||||
github.com/sagernet/quic-go v0.0.0-20230825040534-0cd917b2ddda/go.mod h1:Iw8Tt3dMqC/61cMHa0nN5i/958oYuuMnQCMOSPx+xcg=
|
github.com/sagernet/quic-go v0.0.0-20230831052420-45809eee2e86/go.mod h1:O4Cj7TmMOvqD6S0XMqJRZfcYzA3m0H0ARbbaJFB0p7A=
|
||||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
|
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
|
||||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
|
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
|
||||||
github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
|
github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
|
||||||
@ -171,7 +169,6 @@ github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695AP
|
|||||||
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
|
||||||
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
||||||
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||||
github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
|
github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
|
||||||
@ -193,13 +190,12 @@ golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaE
|
|||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||||
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
||||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
@ -207,14 +203,12 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
|
||||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
||||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@ -224,23 +218,21 @@ golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
|
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
|
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@ -248,17 +240,16 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
|||||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
||||||
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
|
||||||
google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
|
google.golang.org/grpc v1.58.0 h1:32JY8YpPMSR45K+c3o6b8VL73V+rR8k+DeMIr4vRH8o=
|
||||||
google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
|
google.golang.org/grpc v1.58.0/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||||
|
186
test/hysteria2_test.go
Normal file
186
test/hysteria2_test.go
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing-box/transport/hysteria2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHysteria2Self(t *testing.T) {
|
||||||
|
t.Run("self", func(t *testing.T) {
|
||||||
|
testHysteria2Self(t, "")
|
||||||
|
})
|
||||||
|
t.Run("self-salamander", func(t *testing.T) {
|
||||||
|
testHysteria2Self(t, "password")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHysteria2Self(t *testing.T, salamanderPassword string) {
|
||||||
|
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
||||||
|
var obfs *option.Hysteria2Obfs
|
||||||
|
if salamanderPassword != "" {
|
||||||
|
obfs = &option.Hysteria2Obfs{
|
||||||
|
Type: hysteria2.ObfsTypeSalamander,
|
||||||
|
Password: salamanderPassword,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
startInstance(t, option.Options{
|
||||||
|
Inbounds: []option.Inbound{
|
||||||
|
{
|
||||||
|
Type: C.TypeMixed,
|
||||||
|
Tag: "mixed-in",
|
||||||
|
MixedOptions: option.HTTPMixedInboundOptions{
|
||||||
|
ListenOptions: option.ListenOptions{
|
||||||
|
Listen: option.NewListenAddress(netip.IPv4Unspecified()),
|
||||||
|
ListenPort: clientPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.TypeHysteria2,
|
||||||
|
Hysteria2Options: option.Hysteria2InboundOptions{
|
||||||
|
ListenOptions: option.ListenOptions{
|
||||||
|
Listen: option.NewListenAddress(netip.IPv4Unspecified()),
|
||||||
|
ListenPort: serverPort,
|
||||||
|
},
|
||||||
|
Obfs: obfs,
|
||||||
|
Users: []option.Hysteria2User{{
|
||||||
|
Password: "password",
|
||||||
|
}},
|
||||||
|
TLS: &option.InboundTLSOptions{
|
||||||
|
Enabled: true,
|
||||||
|
ServerName: "example.org",
|
||||||
|
CertificatePath: certPem,
|
||||||
|
KeyPath: keyPem,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outbounds: []option.Outbound{
|
||||||
|
{
|
||||||
|
Type: C.TypeDirect,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: C.TypeHysteria2,
|
||||||
|
Tag: "hy2-out",
|
||||||
|
Hysteria2Options: option.Hysteria2OutboundOptions{
|
||||||
|
ServerOptions: option.ServerOptions{
|
||||||
|
Server: "127.0.0.1",
|
||||||
|
ServerPort: serverPort,
|
||||||
|
},
|
||||||
|
Obfs: obfs,
|
||||||
|
Password: "password",
|
||||||
|
TLS: &option.OutboundTLSOptions{
|
||||||
|
Enabled: true,
|
||||||
|
ServerName: "example.org",
|
||||||
|
CertificatePath: certPem,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Route: &option.RouteOptions{
|
||||||
|
Rules: []option.Rule{
|
||||||
|
{
|
||||||
|
Type: C.RuleTypeDefault,
|
||||||
|
DefaultOptions: option.DefaultRule{
|
||||||
|
Inbound: []string{"mixed-in"},
|
||||||
|
Outbound: "hy2-out",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
testSuit(t, clientPort, testPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHysteria2Inbound(t *testing.T) {
|
||||||
|
caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
||||||
|
startInstance(t, option.Options{
|
||||||
|
Inbounds: []option.Inbound{
|
||||||
|
{
|
||||||
|
Type: C.TypeHysteria2,
|
||||||
|
Hysteria2Options: option.Hysteria2InboundOptions{
|
||||||
|
ListenOptions: option.ListenOptions{
|
||||||
|
Listen: option.NewListenAddress(netip.IPv4Unspecified()),
|
||||||
|
ListenPort: serverPort,
|
||||||
|
},
|
||||||
|
Obfs: &option.Hysteria2Obfs{
|
||||||
|
Type: hysteria2.ObfsTypeSalamander,
|
||||||
|
Password: "cry_me_a_r1ver",
|
||||||
|
},
|
||||||
|
Users: []option.Hysteria2User{{
|
||||||
|
Password: "password",
|
||||||
|
}},
|
||||||
|
TLS: &option.InboundTLSOptions{
|
||||||
|
Enabled: true,
|
||||||
|
ServerName: "example.org",
|
||||||
|
CertificatePath: certPem,
|
||||||
|
KeyPath: keyPem,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
startDockerContainer(t, DockerOptions{
|
||||||
|
Image: ImageHysteria2,
|
||||||
|
Ports: []uint16{serverPort, clientPort},
|
||||||
|
Cmd: []string{"client", "-c", "/etc/hysteria/config.yml", "--disable-update-check", "--log-level", "debug"},
|
||||||
|
Bind: map[string]string{
|
||||||
|
"hysteria2-client.yml": "/etc/hysteria/config.yml",
|
||||||
|
caPem: "/etc/hysteria/ca.pem",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
testSuit(t, clientPort, testPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHysteria2Outbound(t *testing.T) {
|
||||||
|
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
|
||||||
|
startDockerContainer(t, DockerOptions{
|
||||||
|
Image: ImageHysteria2,
|
||||||
|
Ports: []uint16{testPort},
|
||||||
|
Cmd: []string{"server", "-c", "/etc/hysteria/config.yml", "--disable-update-check", "--log-level", "debug"},
|
||||||
|
Bind: map[string]string{
|
||||||
|
"hysteria2-server.yml": "/etc/hysteria/config.yml",
|
||||||
|
certPem: "/etc/hysteria/cert.pem",
|
||||||
|
keyPem: "/etc/hysteria/key.pem",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
startInstance(t, option.Options{
|
||||||
|
Inbounds: []option.Inbound{
|
||||||
|
{
|
||||||
|
Type: C.TypeMixed,
|
||||||
|
MixedOptions: option.HTTPMixedInboundOptions{
|
||||||
|
ListenOptions: option.ListenOptions{
|
||||||
|
Listen: option.NewListenAddress(netip.IPv4Unspecified()),
|
||||||
|
ListenPort: clientPort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Outbounds: []option.Outbound{
|
||||||
|
{
|
||||||
|
Type: C.TypeHysteria2,
|
||||||
|
Hysteria2Options: option.Hysteria2OutboundOptions{
|
||||||
|
ServerOptions: option.ServerOptions{
|
||||||
|
Server: "127.0.0.1",
|
||||||
|
ServerPort: serverPort,
|
||||||
|
},
|
||||||
|
Obfs: &option.Hysteria2Obfs{
|
||||||
|
Type: hysteria2.ObfsTypeSalamander,
|
||||||
|
Password: "cry_me_a_r1ver",
|
||||||
|
},
|
||||||
|
Password: "password",
|
||||||
|
TLS: &option.OutboundTLSOptions{
|
||||||
|
Enabled: true,
|
||||||
|
ServerName: "example.org",
|
||||||
|
CertificatePath: certPem,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
testSuitSimple1(t, clientPort, testPort)
|
||||||
|
}
|
314
transport/hysteria2/client.go
Normal file
314
transport/hysteria2/client.go
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
package hysteria2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/quic-go"
|
||||||
|
"github.com/sagernet/sing-box/common/qtls"
|
||||||
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
|
"github.com/sagernet/sing-box/transport/hysteria2/congestion"
|
||||||
|
"github.com/sagernet/sing-box/transport/hysteria2/internal/protocol"
|
||||||
|
tuicCongestion "github.com/sagernet/sing-box/transport/tuic/congestion"
|
||||||
|
"github.com/sagernet/sing/common/baderror"
|
||||||
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultStreamReceiveWindow = 8388608 // 8MB
|
||||||
|
defaultConnReceiveWindow = defaultStreamReceiveWindow * 5 / 2 // 20MB
|
||||||
|
defaultMaxIdleTimeout = 30 * time.Second
|
||||||
|
defaultKeepAlivePeriod = 10 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientOptions struct {
|
||||||
|
Context context.Context
|
||||||
|
Dialer N.Dialer
|
||||||
|
ServerAddress M.Socksaddr
|
||||||
|
SendBPS uint64
|
||||||
|
ReceiveBPS uint64
|
||||||
|
SalamanderPassword string
|
||||||
|
Password string
|
||||||
|
TLSConfig tls.Config
|
||||||
|
UDPDisabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
ctx context.Context
|
||||||
|
dialer N.Dialer
|
||||||
|
serverAddr M.Socksaddr
|
||||||
|
sendBPS uint64
|
||||||
|
receiveBPS uint64
|
||||||
|
salamanderPassword string
|
||||||
|
password string
|
||||||
|
tlsConfig tls.Config
|
||||||
|
quicConfig *quic.Config
|
||||||
|
udpDisabled bool
|
||||||
|
|
||||||
|
connAccess sync.RWMutex
|
||||||
|
conn *clientQUICConnection
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(options ClientOptions) (*Client, error) {
|
||||||
|
quicConfig := &quic.Config{
|
||||||
|
DisablePathMTUDiscovery: !(runtime.GOOS == "windows" || runtime.GOOS == "linux" || runtime.GOOS == "android" || runtime.GOOS == "darwin"),
|
||||||
|
MaxDatagramFrameSize: 1400,
|
||||||
|
EnableDatagrams: true,
|
||||||
|
InitialStreamReceiveWindow: defaultStreamReceiveWindow,
|
||||||
|
MaxStreamReceiveWindow: defaultStreamReceiveWindow,
|
||||||
|
InitialConnectionReceiveWindow: defaultConnReceiveWindow,
|
||||||
|
MaxConnectionReceiveWindow: defaultConnReceiveWindow,
|
||||||
|
MaxIdleTimeout: defaultMaxIdleTimeout,
|
||||||
|
KeepAlivePeriod: defaultKeepAlivePeriod,
|
||||||
|
}
|
||||||
|
return &Client{
|
||||||
|
ctx: options.Context,
|
||||||
|
dialer: options.Dialer,
|
||||||
|
serverAddr: options.ServerAddress,
|
||||||
|
sendBPS: options.SendBPS,
|
||||||
|
receiveBPS: options.ReceiveBPS,
|
||||||
|
salamanderPassword: options.SalamanderPassword,
|
||||||
|
password: options.Password,
|
||||||
|
tlsConfig: options.TLSConfig,
|
||||||
|
quicConfig: quicConfig,
|
||||||
|
udpDisabled: options.UDPDisabled,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) offer(ctx context.Context) (*clientQUICConnection, error) {
|
||||||
|
conn := c.conn
|
||||||
|
if conn != nil && conn.active() {
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
c.connAccess.Lock()
|
||||||
|
defer c.connAccess.Unlock()
|
||||||
|
conn = c.conn
|
||||||
|
if conn != nil && conn.active() {
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
conn, err := c.offerNew(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) offerNew(ctx context.Context) (*clientQUICConnection, error) {
|
||||||
|
udpConn, err := c.dialer.DialContext(ctx, "udp", c.serverAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var packetConn net.PacketConn
|
||||||
|
packetConn = bufio.NewUnbindPacketConn(udpConn)
|
||||||
|
if c.salamanderPassword != "" {
|
||||||
|
packetConn = NewSalamanderConn(packetConn, []byte(c.salamanderPassword))
|
||||||
|
}
|
||||||
|
var quicConn quic.EarlyConnection
|
||||||
|
http3Transport, err := qtls.CreateTransport(packetConn, &quicConn, c.serverAddr, c.tlsConfig, c.quicConfig, true)
|
||||||
|
if err != nil {
|
||||||
|
udpConn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
request := &http.Request{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
URL: &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: protocol.URLHost,
|
||||||
|
Path: protocol.URLPath,
|
||||||
|
},
|
||||||
|
Header: make(http.Header),
|
||||||
|
}
|
||||||
|
protocol.AuthRequestToHeader(request.Header, protocol.AuthRequest{Auth: c.password, Rx: c.receiveBPS})
|
||||||
|
response, err := http3Transport.RoundTrip(request)
|
||||||
|
if err != nil {
|
||||||
|
if quicConn != nil {
|
||||||
|
quicConn.CloseWithError(0, "")
|
||||||
|
}
|
||||||
|
udpConn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if response.StatusCode != protocol.StatusAuthOK {
|
||||||
|
if quicConn != nil {
|
||||||
|
quicConn.CloseWithError(0, "")
|
||||||
|
}
|
||||||
|
udpConn.Close()
|
||||||
|
return nil, E.New("authentication failed, status code: ", response.StatusCode)
|
||||||
|
}
|
||||||
|
response.Body.Close()
|
||||||
|
authResponse := protocol.AuthResponseFromHeader(response.Header)
|
||||||
|
actualTx := authResponse.Rx
|
||||||
|
if actualTx == 0 || actualTx > c.sendBPS {
|
||||||
|
actualTx = c.sendBPS
|
||||||
|
}
|
||||||
|
if !authResponse.RxAuto && actualTx > 0 {
|
||||||
|
quicConn.SetCongestionControl(congestion.NewBrutalSender(actualTx))
|
||||||
|
} else {
|
||||||
|
quicConn.SetCongestionControl(tuicCongestion.NewBBRSender(
|
||||||
|
tuicCongestion.DefaultClock{},
|
||||||
|
tuicCongestion.GetInitialPacketSize(quicConn.RemoteAddr()),
|
||||||
|
tuicCongestion.InitialCongestionWindow*tuicCongestion.InitialMaxDatagramSize,
|
||||||
|
tuicCongestion.DefaultBBRMaxCongestionWindow*tuicCongestion.InitialMaxDatagramSize,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
conn := &clientQUICConnection{
|
||||||
|
quicConn: quicConn,
|
||||||
|
rawConn: udpConn,
|
||||||
|
connDone: make(chan struct{}),
|
||||||
|
udpDisabled: c.udpDisabled || !authResponse.UDPEnabled,
|
||||||
|
udpConnMap: make(map[uint32]*udpPacketConn),
|
||||||
|
}
|
||||||
|
if !c.udpDisabled {
|
||||||
|
go c.loopMessages(conn)
|
||||||
|
}
|
||||||
|
c.conn = conn
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DialConn(ctx context.Context, destination M.Socksaddr) (net.Conn, error) {
|
||||||
|
conn, err := c.offer(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stream, err := conn.quicConn.OpenStream()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &clientConn{
|
||||||
|
Stream: stream,
|
||||||
|
destination: destination,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ListenPacket(ctx context.Context) (net.PacketConn, error) {
|
||||||
|
if c.udpDisabled {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
conn, err := c.offer(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if conn.udpDisabled {
|
||||||
|
return nil, E.New("UDP disabled by server")
|
||||||
|
}
|
||||||
|
var sessionID uint32
|
||||||
|
clientPacketConn := newUDPPacketConn(ctx, conn.quicConn, func() {
|
||||||
|
conn.udpAccess.Lock()
|
||||||
|
delete(conn.udpConnMap, sessionID)
|
||||||
|
conn.udpAccess.Unlock()
|
||||||
|
})
|
||||||
|
conn.udpAccess.Lock()
|
||||||
|
sessionID = conn.udpSessionID
|
||||||
|
conn.udpSessionID++
|
||||||
|
conn.udpConnMap[sessionID] = clientPacketConn
|
||||||
|
conn.udpAccess.Unlock()
|
||||||
|
clientPacketConn.sessionID = sessionID
|
||||||
|
return clientPacketConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) CloseWithError(err error) error {
|
||||||
|
conn := c.conn
|
||||||
|
if conn != nil {
|
||||||
|
conn.closeWithError(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientQUICConnection struct {
|
||||||
|
quicConn quic.Connection
|
||||||
|
rawConn io.Closer
|
||||||
|
closeOnce sync.Once
|
||||||
|
connDone chan struct{}
|
||||||
|
connErr error
|
||||||
|
udpDisabled bool
|
||||||
|
udpAccess sync.RWMutex
|
||||||
|
udpConnMap map[uint32]*udpPacketConn
|
||||||
|
udpSessionID uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientQUICConnection) active() bool {
|
||||||
|
select {
|
||||||
|
case <-c.quicConn.Context().Done():
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-c.connDone:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientQUICConnection) closeWithError(err error) {
|
||||||
|
c.closeOnce.Do(func() {
|
||||||
|
c.connErr = err
|
||||||
|
close(c.connDone)
|
||||||
|
c.quicConn.CloseWithError(0, "")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientConn struct {
|
||||||
|
quic.Stream
|
||||||
|
destination M.Socksaddr
|
||||||
|
requestWritten bool
|
||||||
|
responseRead bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientConn) NeedHandshake() bool {
|
||||||
|
return !c.requestWritten
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientConn) Read(p []byte) (n int, err error) {
|
||||||
|
if c.responseRead {
|
||||||
|
return c.Stream.Read(p)
|
||||||
|
}
|
||||||
|
status, errorMessage, err := protocol.ReadTCPResponse(c.Stream)
|
||||||
|
if err != nil {
|
||||||
|
return 0, baderror.WrapQUIC(err)
|
||||||
|
}
|
||||||
|
if !status {
|
||||||
|
err = E.New("remote error: ", errorMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.responseRead = true
|
||||||
|
n, err = c.Stream.Read(p)
|
||||||
|
return n, baderror.WrapQUIC(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientConn) Write(p []byte) (n int, err error) {
|
||||||
|
if !c.requestWritten {
|
||||||
|
buffer := protocol.WriteTCPRequest(c.destination.String(), p)
|
||||||
|
defer buffer.Release()
|
||||||
|
_, err = c.Stream.Write(buffer.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.requestWritten = true
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
n, err = c.Stream.Write(p)
|
||||||
|
return n, baderror.WrapQUIC(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientConn) LocalAddr() net.Addr {
|
||||||
|
return M.Socksaddr{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientConn) RemoteAddr() net.Addr {
|
||||||
|
return M.Socksaddr{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientConn) Close() error {
|
||||||
|
c.Stream.CancelRead(0)
|
||||||
|
return c.Stream.Close()
|
||||||
|
}
|
47
transport/hysteria2/client_paclet.go
Normal file
47
transport/hysteria2/client_paclet.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package hysteria2
|
||||||
|
|
||||||
|
import E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
|
func (c *Client) loopMessages(conn *clientQUICConnection) {
|
||||||
|
for {
|
||||||
|
message, err := conn.quicConn.ReceiveMessage(c.ctx)
|
||||||
|
if err != nil {
|
||||||
|
conn.closeWithError(E.Cause(err, "receive message"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
hErr := c.handleMessage(conn, message)
|
||||||
|
if hErr != nil {
|
||||||
|
conn.closeWithError(E.Cause(hErr, "handle message"))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleMessage(conn *clientQUICConnection, data []byte) error {
|
||||||
|
message := allocMessage()
|
||||||
|
err := decodeUDPMessage(message, data)
|
||||||
|
if err != nil {
|
||||||
|
message.release()
|
||||||
|
return E.Cause(err, "decode UDP message")
|
||||||
|
}
|
||||||
|
conn.handleUDPMessage(message)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientQUICConnection) handleUDPMessage(message *udpMessage) {
|
||||||
|
c.udpAccess.RLock()
|
||||||
|
udpConn, loaded := c.udpConnMap[message.sessionID]
|
||||||
|
c.udpAccess.RUnlock()
|
||||||
|
if !loaded {
|
||||||
|
message.releaseMessage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-udpConn.ctx.Done():
|
||||||
|
message.releaseMessage()
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
udpConn.inputPacket(message)
|
||||||
|
}
|
151
transport/hysteria2/congestion/brutal.go
Normal file
151
transport/hysteria2/congestion/brutal.go
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
package congestion
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/quic-go/congestion"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
initMaxDatagramSize = 1252
|
||||||
|
|
||||||
|
pktInfoSlotCount = 4
|
||||||
|
minSampleCount = 50
|
||||||
|
minAckRate = 0.8
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ congestion.CongestionControl = &BrutalSender{}
|
||||||
|
|
||||||
|
type BrutalSender struct {
|
||||||
|
rttStats congestion.RTTStatsProvider
|
||||||
|
bps congestion.ByteCount
|
||||||
|
maxDatagramSize congestion.ByteCount
|
||||||
|
pacer *pacer
|
||||||
|
|
||||||
|
pktInfoSlots [pktInfoSlotCount]pktInfo
|
||||||
|
ackRate float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type pktInfo struct {
|
||||||
|
Timestamp int64
|
||||||
|
AckCount uint64
|
||||||
|
LossCount uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBrutalSender(bps uint64) *BrutalSender {
|
||||||
|
bs := &BrutalSender{
|
||||||
|
bps: congestion.ByteCount(bps),
|
||||||
|
maxDatagramSize: initMaxDatagramSize,
|
||||||
|
ackRate: 1,
|
||||||
|
}
|
||||||
|
bs.pacer = newPacer(func() congestion.ByteCount {
|
||||||
|
return congestion.ByteCount(float64(bs.bps) / bs.ackRate)
|
||||||
|
})
|
||||||
|
return bs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BrutalSender) SetRTTStatsProvider(rttStats congestion.RTTStatsProvider) {
|
||||||
|
b.rttStats = rttStats
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BrutalSender) TimeUntilSend(bytesInFlight congestion.ByteCount) time.Time {
|
||||||
|
return b.pacer.TimeUntilSend()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BrutalSender) HasPacingBudget(now time.Time) bool {
|
||||||
|
return b.pacer.Budget(now) >= b.maxDatagramSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BrutalSender) CanSend(bytesInFlight congestion.ByteCount) bool {
|
||||||
|
return bytesInFlight < b.GetCongestionWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BrutalSender) GetCongestionWindow() congestion.ByteCount {
|
||||||
|
rtt := b.rttStats.SmoothedRTT()
|
||||||
|
if rtt <= 0 {
|
||||||
|
return 10240
|
||||||
|
}
|
||||||
|
return congestion.ByteCount(float64(b.bps) * rtt.Seconds() * 1.5 / b.ackRate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BrutalSender) OnPacketSent(sentTime time.Time, bytesInFlight congestion.ByteCount,
|
||||||
|
packetNumber congestion.PacketNumber, bytes congestion.ByteCount, isRetransmittable bool,
|
||||||
|
) {
|
||||||
|
b.pacer.SentPacket(sentTime, bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BrutalSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes congestion.ByteCount,
|
||||||
|
priorInFlight congestion.ByteCount, eventTime time.Time,
|
||||||
|
) {
|
||||||
|
currentTimestamp := eventTime.Unix()
|
||||||
|
slot := currentTimestamp % pktInfoSlotCount
|
||||||
|
if b.pktInfoSlots[slot].Timestamp == currentTimestamp {
|
||||||
|
b.pktInfoSlots[slot].AckCount++
|
||||||
|
} else {
|
||||||
|
// uninitialized slot or too old, reset
|
||||||
|
b.pktInfoSlots[slot].Timestamp = currentTimestamp
|
||||||
|
b.pktInfoSlots[slot].AckCount = 1
|
||||||
|
b.pktInfoSlots[slot].LossCount = 0
|
||||||
|
}
|
||||||
|
b.updateAckRate(currentTimestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BrutalSender) OnPacketLost(number congestion.PacketNumber, lostBytes congestion.ByteCount,
|
||||||
|
priorInFlight congestion.ByteCount,
|
||||||
|
) {
|
||||||
|
currentTimestamp := time.Now().Unix()
|
||||||
|
slot := currentTimestamp % pktInfoSlotCount
|
||||||
|
if b.pktInfoSlots[slot].Timestamp == currentTimestamp {
|
||||||
|
b.pktInfoSlots[slot].LossCount++
|
||||||
|
} else {
|
||||||
|
// uninitialized slot or too old, reset
|
||||||
|
b.pktInfoSlots[slot].Timestamp = currentTimestamp
|
||||||
|
b.pktInfoSlots[slot].AckCount = 0
|
||||||
|
b.pktInfoSlots[slot].LossCount = 1
|
||||||
|
}
|
||||||
|
b.updateAckRate(currentTimestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BrutalSender) SetMaxDatagramSize(size congestion.ByteCount) {
|
||||||
|
b.maxDatagramSize = size
|
||||||
|
b.pacer.SetMaxDatagramSize(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BrutalSender) updateAckRate(currentTimestamp int64) {
|
||||||
|
minTimestamp := currentTimestamp - pktInfoSlotCount
|
||||||
|
var ackCount, lossCount uint64
|
||||||
|
for _, info := range b.pktInfoSlots {
|
||||||
|
if info.Timestamp < minTimestamp {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ackCount += info.AckCount
|
||||||
|
lossCount += info.LossCount
|
||||||
|
}
|
||||||
|
if ackCount+lossCount < minSampleCount {
|
||||||
|
b.ackRate = 1
|
||||||
|
}
|
||||||
|
rate := float64(ackCount) / float64(ackCount+lossCount)
|
||||||
|
if rate < minAckRate {
|
||||||
|
b.ackRate = minAckRate
|
||||||
|
}
|
||||||
|
b.ackRate = rate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BrutalSender) InSlowStart() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BrutalSender) InRecovery() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BrutalSender) MaybeExitSlowStart() {}
|
||||||
|
|
||||||
|
func (b *BrutalSender) OnRetransmissionTimeout(packetsRetransmitted bool) {}
|
||||||
|
|
||||||
|
func maxDuration(a, b time.Duration) time.Duration {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
86
transport/hysteria2/congestion/pacer.go
Normal file
86
transport/hysteria2/congestion/pacer.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package congestion
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/quic-go/congestion"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxBurstPackets = 10
|
||||||
|
minPacingDelay = time.Millisecond
|
||||||
|
)
|
||||||
|
|
||||||
|
// The pacer implements a token bucket pacing algorithm.
|
||||||
|
type pacer struct {
|
||||||
|
budgetAtLastSent congestion.ByteCount
|
||||||
|
maxDatagramSize congestion.ByteCount
|
||||||
|
lastSentTime time.Time
|
||||||
|
getBandwidth func() congestion.ByteCount // in bytes/s
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPacer(getBandwidth func() congestion.ByteCount) *pacer {
|
||||||
|
p := &pacer{
|
||||||
|
budgetAtLastSent: maxBurstPackets * initMaxDatagramSize,
|
||||||
|
maxDatagramSize: initMaxDatagramSize,
|
||||||
|
getBandwidth: getBandwidth,
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pacer) SentPacket(sendTime time.Time, size congestion.ByteCount) {
|
||||||
|
budget := p.Budget(sendTime)
|
||||||
|
if size > budget {
|
||||||
|
p.budgetAtLastSent = 0
|
||||||
|
} else {
|
||||||
|
p.budgetAtLastSent = budget - size
|
||||||
|
}
|
||||||
|
p.lastSentTime = sendTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pacer) Budget(now time.Time) congestion.ByteCount {
|
||||||
|
if p.lastSentTime.IsZero() {
|
||||||
|
return p.maxBurstSize()
|
||||||
|
}
|
||||||
|
budget := p.budgetAtLastSent + (p.getBandwidth()*congestion.ByteCount(now.Sub(p.lastSentTime).Nanoseconds()))/1e9
|
||||||
|
return minByteCount(p.maxBurstSize(), budget)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pacer) maxBurstSize() congestion.ByteCount {
|
||||||
|
return maxByteCount(
|
||||||
|
congestion.ByteCount((minPacingDelay+time.Millisecond).Nanoseconds())*p.getBandwidth()/1e9,
|
||||||
|
maxBurstPackets*p.maxDatagramSize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimeUntilSend returns when the next packet should be sent.
|
||||||
|
// It returns the zero value of time.Time if a packet can be sent immediately.
|
||||||
|
func (p *pacer) TimeUntilSend() time.Time {
|
||||||
|
if p.budgetAtLastSent >= p.maxDatagramSize {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
return p.lastSentTime.Add(maxDuration(
|
||||||
|
minPacingDelay,
|
||||||
|
time.Duration(math.Ceil(float64(p.maxDatagramSize-p.budgetAtLastSent)*1e9/
|
||||||
|
float64(p.getBandwidth())))*time.Nanosecond,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pacer) SetMaxDatagramSize(s congestion.ByteCount) {
|
||||||
|
p.maxDatagramSize = s
|
||||||
|
}
|
||||||
|
|
||||||
|
func maxByteCount(a, b congestion.ByteCount) congestion.ByteCount {
|
||||||
|
if a < b {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func minByteCount(a, b congestion.ByteCount) congestion.ByteCount {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
68
transport/hysteria2/internal/protocol/http.go
Normal file
68
transport/hysteria2/internal/protocol/http.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
URLHost = "hysteria"
|
||||||
|
URLPath = "/auth"
|
||||||
|
|
||||||
|
RequestHeaderAuth = "Hysteria-Auth"
|
||||||
|
ResponseHeaderUDPEnabled = "Hysteria-UDP"
|
||||||
|
CommonHeaderCCRX = "Hysteria-CC-RX"
|
||||||
|
CommonHeaderPadding = "Hysteria-Padding"
|
||||||
|
|
||||||
|
StatusAuthOK = 233
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthRequest is what client sends to server for authentication.
|
||||||
|
type AuthRequest struct {
|
||||||
|
Auth string
|
||||||
|
Rx uint64 // 0 = unknown, client asks server to use bandwidth detection
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthResponse is what server sends to client when authentication is passed.
|
||||||
|
type AuthResponse struct {
|
||||||
|
UDPEnabled bool
|
||||||
|
Rx uint64 // 0 = unlimited
|
||||||
|
RxAuto bool // true = server asks client to use bandwidth detection
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthRequestFromHeader(h http.Header) AuthRequest {
|
||||||
|
rx, _ := strconv.ParseUint(h.Get(CommonHeaderCCRX), 10, 64)
|
||||||
|
return AuthRequest{
|
||||||
|
Auth: h.Get(RequestHeaderAuth),
|
||||||
|
Rx: rx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthRequestToHeader(h http.Header, req AuthRequest) {
|
||||||
|
h.Set(RequestHeaderAuth, req.Auth)
|
||||||
|
h.Set(CommonHeaderCCRX, strconv.FormatUint(req.Rx, 10))
|
||||||
|
h.Set(CommonHeaderPadding, authRequestPadding.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthResponseFromHeader(h http.Header) AuthResponse {
|
||||||
|
resp := AuthResponse{}
|
||||||
|
resp.UDPEnabled, _ = strconv.ParseBool(h.Get(ResponseHeaderUDPEnabled))
|
||||||
|
rxStr := h.Get(CommonHeaderCCRX)
|
||||||
|
if rxStr == "auto" {
|
||||||
|
// Special case for server requesting client to use bandwidth detection
|
||||||
|
resp.RxAuto = true
|
||||||
|
} else {
|
||||||
|
resp.Rx, _ = strconv.ParseUint(rxStr, 10, 64)
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthResponseToHeader(h http.Header, resp AuthResponse) {
|
||||||
|
h.Set(ResponseHeaderUDPEnabled, strconv.FormatBool(resp.UDPEnabled))
|
||||||
|
if resp.RxAuto {
|
||||||
|
h.Set(CommonHeaderCCRX, "auto")
|
||||||
|
} else {
|
||||||
|
h.Set(CommonHeaderCCRX, strconv.FormatUint(resp.Rx, 10))
|
||||||
|
}
|
||||||
|
h.Set(CommonHeaderPadding, authResponsePadding.String())
|
||||||
|
}
|
31
transport/hysteria2/internal/protocol/padding.go
Normal file
31
transport/hysteria2/internal/protocol/padding.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
paddingChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
)
|
||||||
|
|
||||||
|
// padding specifies a half-open range [Min, Max).
|
||||||
|
type padding struct {
|
||||||
|
Min int
|
||||||
|
Max int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p padding) String() string {
|
||||||
|
n := p.Min + rand.Intn(p.Max-p.Min)
|
||||||
|
bs := make([]byte, n)
|
||||||
|
for i := range bs {
|
||||||
|
bs[i] = paddingChars[rand.Intn(len(paddingChars))]
|
||||||
|
}
|
||||||
|
return string(bs)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
authRequestPadding = padding{Min: 256, Max: 2048}
|
||||||
|
authResponsePadding = padding{Min: 256, Max: 2048}
|
||||||
|
tcpRequestPadding = padding{Min: 64, Max: 512}
|
||||||
|
tcpResponsePadding = padding{Min: 128, Max: 1024}
|
||||||
|
)
|
266
transport/hysteria2/internal/protocol/proxy.go
Normal file
266
transport/hysteria2/internal/protocol/proxy.go
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/sagernet/quic-go/quicvarint"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/buf"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/rw"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FrameTypeTCPRequest = 0x401
|
||||||
|
|
||||||
|
// Max length values are for preventing DoS attacks
|
||||||
|
|
||||||
|
MaxAddressLength = 2048
|
||||||
|
MaxMessageLength = 2048
|
||||||
|
MaxPaddingLength = 4096
|
||||||
|
|
||||||
|
MaxUDPSize = 4096
|
||||||
|
|
||||||
|
maxVarInt1 = 63
|
||||||
|
maxVarInt2 = 16383
|
||||||
|
maxVarInt4 = 1073741823
|
||||||
|
maxVarInt8 = 4611686018427387903
|
||||||
|
)
|
||||||
|
|
||||||
|
// TCPRequest format:
|
||||||
|
// 0x401 (QUIC varint)
|
||||||
|
// Address length (QUIC varint)
|
||||||
|
// Address (bytes)
|
||||||
|
// Padding length (QUIC varint)
|
||||||
|
// Padding (bytes)
|
||||||
|
|
||||||
|
func ReadTCPRequest(r io.Reader) (string, error) {
|
||||||
|
bReader := quicvarint.NewReader(r)
|
||||||
|
addrLen, err := quicvarint.Read(bReader)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if addrLen == 0 || addrLen > MaxAddressLength {
|
||||||
|
return "", E.New("invalid address length")
|
||||||
|
}
|
||||||
|
addrBuf := make([]byte, addrLen)
|
||||||
|
_, err = io.ReadFull(r, addrBuf)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
paddingLen, err := quicvarint.Read(bReader)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if paddingLen > MaxPaddingLength {
|
||||||
|
return "", E.New("invalid padding length")
|
||||||
|
}
|
||||||
|
if paddingLen > 0 {
|
||||||
|
_, err = io.CopyN(io.Discard, r, int64(paddingLen))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(addrBuf), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteTCPRequest(addr string, payload []byte) *buf.Buffer {
|
||||||
|
padding := tcpRequestPadding.String()
|
||||||
|
paddingLen := len(padding)
|
||||||
|
addrLen := len(addr)
|
||||||
|
sz := int(quicvarint.Len(FrameTypeTCPRequest)) +
|
||||||
|
int(quicvarint.Len(uint64(addrLen))) + addrLen +
|
||||||
|
int(quicvarint.Len(uint64(paddingLen))) + paddingLen
|
||||||
|
buffer := buf.NewSize(sz + len(payload))
|
||||||
|
bufferContent := buffer.Extend(sz)
|
||||||
|
i := varintPut(bufferContent, FrameTypeTCPRequest)
|
||||||
|
i += varintPut(bufferContent[i:], uint64(addrLen))
|
||||||
|
i += copy(bufferContent[i:], addr)
|
||||||
|
i += varintPut(bufferContent[i:], uint64(paddingLen))
|
||||||
|
copy(bufferContent[i:], padding)
|
||||||
|
buffer.Write(payload)
|
||||||
|
return buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// TCPResponse format:
|
||||||
|
// Status (byte, 0=ok, 1=error)
|
||||||
|
// Message length (QUIC varint)
|
||||||
|
// Message (bytes)
|
||||||
|
// Padding length (QUIC varint)
|
||||||
|
// Padding (bytes)
|
||||||
|
|
||||||
|
func ReadTCPResponse(r io.Reader) (bool, string, error) {
|
||||||
|
var status [1]byte
|
||||||
|
if _, err := io.ReadFull(r, status[:]); err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
bReader := quicvarint.NewReader(r)
|
||||||
|
msg, err := ReadVString(bReader)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
paddingLen, err := quicvarint.Read(bReader)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
if paddingLen > MaxPaddingLength {
|
||||||
|
return false, "", E.New("invalid padding length")
|
||||||
|
}
|
||||||
|
if paddingLen > 0 {
|
||||||
|
_, err = io.CopyN(io.Discard, r, int64(paddingLen))
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return status[0] == 0, msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteTCPResponse(ok bool, msg string, payload []byte) *buf.Buffer {
|
||||||
|
padding := tcpResponsePadding.String()
|
||||||
|
paddingLen := len(padding)
|
||||||
|
msgLen := len(msg)
|
||||||
|
sz := 1 + int(quicvarint.Len(uint64(msgLen))) + msgLen +
|
||||||
|
int(quicvarint.Len(uint64(paddingLen))) + paddingLen
|
||||||
|
buffer := buf.NewSize(sz + len(payload))
|
||||||
|
if ok {
|
||||||
|
buffer.WriteByte(0)
|
||||||
|
} else {
|
||||||
|
buffer.WriteByte(1)
|
||||||
|
}
|
||||||
|
WriteVString(buffer, msg)
|
||||||
|
WriteUVariant(buffer, uint64(paddingLen))
|
||||||
|
buffer.Extend(paddingLen)
|
||||||
|
buffer.Write(payload)
|
||||||
|
return buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// UDPMessage format:
|
||||||
|
// Session ID (uint32 BE)
|
||||||
|
// Packet ID (uint16 BE)
|
||||||
|
// Fragment ID (uint8)
|
||||||
|
// Fragment count (uint8)
|
||||||
|
// Address length (QUIC varint)
|
||||||
|
// Address (bytes)
|
||||||
|
// Data...
|
||||||
|
|
||||||
|
type UDPMessage struct {
|
||||||
|
SessionID uint32 // 4
|
||||||
|
PacketID uint16 // 2
|
||||||
|
FragID uint8 // 1
|
||||||
|
FragCount uint8 // 1
|
||||||
|
Addr string // varint + bytes
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UDPMessage) HeaderSize() int {
|
||||||
|
lAddr := len(m.Addr)
|
||||||
|
return 4 + 2 + 1 + 1 + int(quicvarint.Len(uint64(lAddr))) + lAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UDPMessage) Size() int {
|
||||||
|
return m.HeaderSize() + len(m.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UDPMessage) Serialize(buf []byte) int {
|
||||||
|
// Make sure the buffer is big enough
|
||||||
|
if len(buf) < m.Size() {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
binary.BigEndian.PutUint32(buf, m.SessionID)
|
||||||
|
binary.BigEndian.PutUint16(buf[4:], m.PacketID)
|
||||||
|
buf[6] = m.FragID
|
||||||
|
buf[7] = m.FragCount
|
||||||
|
i := varintPut(buf[8:], uint64(len(m.Addr)))
|
||||||
|
i += copy(buf[8+i:], m.Addr)
|
||||||
|
i += copy(buf[8+i:], m.Data)
|
||||||
|
return 8 + i
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseUDPMessage(msg []byte) (*UDPMessage, error) {
|
||||||
|
m := &UDPMessage{}
|
||||||
|
buf := bytes.NewBuffer(msg)
|
||||||
|
if err := binary.Read(buf, binary.BigEndian, &m.SessionID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := binary.Read(buf, binary.BigEndian, &m.PacketID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := binary.Read(buf, binary.BigEndian, &m.FragID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := binary.Read(buf, binary.BigEndian, &m.FragCount); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
lAddr, err := quicvarint.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if lAddr == 0 || lAddr > MaxMessageLength {
|
||||||
|
return nil, E.New("invalid address length")
|
||||||
|
}
|
||||||
|
bs := buf.Bytes()
|
||||||
|
m.Addr = string(bs[:lAddr])
|
||||||
|
m.Data = bs[lAddr:]
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadVString(reader io.Reader) (string, error) {
|
||||||
|
length, err := quicvarint.Read(quicvarint.NewReader(reader))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
value, err := rw.ReadBytes(reader, int(length))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(value), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteVString(writer io.Writer, value string) error {
|
||||||
|
err := WriteUVariant(writer, uint64(len(value)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return rw.WriteString(writer, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteUVariant(writer io.Writer, value uint64) error {
|
||||||
|
var b [8]byte
|
||||||
|
return common.Error(writer.Write(b[:varintPut(b[:], value)]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// varintPut is like quicvarint.Append, but instead of appending to a slice,
|
||||||
|
// it writes to a fixed-size buffer. Returns the number of bytes written.
|
||||||
|
func varintPut(b []byte, i uint64) int {
|
||||||
|
if i <= maxVarInt1 {
|
||||||
|
b[0] = uint8(i)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if i <= maxVarInt2 {
|
||||||
|
b[0] = uint8(i>>8) | 0x40
|
||||||
|
b[1] = uint8(i)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
if i <= maxVarInt4 {
|
||||||
|
b[0] = uint8(i>>24) | 0x80
|
||||||
|
b[1] = uint8(i >> 16)
|
||||||
|
b[2] = uint8(i >> 8)
|
||||||
|
b[3] = uint8(i)
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
if i <= maxVarInt8 {
|
||||||
|
b[0] = uint8(i>>56) | 0xc0
|
||||||
|
b[1] = uint8(i >> 48)
|
||||||
|
b[2] = uint8(i >> 40)
|
||||||
|
b[3] = uint8(i >> 32)
|
||||||
|
b[4] = uint8(i >> 24)
|
||||||
|
b[5] = uint8(i >> 16)
|
||||||
|
b[6] = uint8(i >> 8)
|
||||||
|
b[7] = uint8(i)
|
||||||
|
return 8
|
||||||
|
}
|
||||||
|
panic(fmt.Sprintf("%#x doesn't fit into 62 bits", i))
|
||||||
|
}
|
450
transport/hysteria2/packet.go
Normal file
450
transport/hysteria2/packet.go
Normal file
@ -0,0 +1,450 @@
|
|||||||
|
package hysteria2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/quic-go"
|
||||||
|
"github.com/sagernet/quic-go/quicvarint"
|
||||||
|
"github.com/sagernet/sing-box/transport/hysteria2/internal/protocol"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/atomic"
|
||||||
|
"github.com/sagernet/sing/common/buf"
|
||||||
|
"github.com/sagernet/sing/common/cache"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
var udpMessagePool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return new(udpMessage)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func allocMessage() *udpMessage {
|
||||||
|
message := udpMessagePool.Get().(*udpMessage)
|
||||||
|
message.referenced = true
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
func releaseMessages(messages []*udpMessage) {
|
||||||
|
for _, message := range messages {
|
||||||
|
if message != nil {
|
||||||
|
message.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type udpMessage struct {
|
||||||
|
sessionID uint32
|
||||||
|
packetID uint16
|
||||||
|
fragmentID uint8
|
||||||
|
fragmentTotal uint8
|
||||||
|
destination string
|
||||||
|
data *buf.Buffer
|
||||||
|
referenced bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *udpMessage) release() {
|
||||||
|
if !m.referenced {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*m = udpMessage{}
|
||||||
|
udpMessagePool.Put(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *udpMessage) releaseMessage() {
|
||||||
|
m.data.Release()
|
||||||
|
m.release()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *udpMessage) pack() *buf.Buffer {
|
||||||
|
buffer := buf.NewSize(m.headerSize() + m.data.Len())
|
||||||
|
common.Must(
|
||||||
|
binary.Write(buffer, binary.BigEndian, m.sessionID),
|
||||||
|
binary.Write(buffer, binary.BigEndian, m.packetID),
|
||||||
|
binary.Write(buffer, binary.BigEndian, m.fragmentID),
|
||||||
|
binary.Write(buffer, binary.BigEndian, m.fragmentTotal),
|
||||||
|
protocol.WriteVString(buffer, m.destination),
|
||||||
|
common.Error(buffer.Write(m.data.Bytes())),
|
||||||
|
)
|
||||||
|
return buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *udpMessage) headerSize() int {
|
||||||
|
return 8 + int(quicvarint.Len(uint64(len(m.destination)))) + len(m.destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fragUDPMessage(message *udpMessage, maxPacketSize int) []*udpMessage {
|
||||||
|
if message.data.Len() <= maxPacketSize {
|
||||||
|
return []*udpMessage{message}
|
||||||
|
}
|
||||||
|
var fragments []*udpMessage
|
||||||
|
originPacket := message.data.Bytes()
|
||||||
|
udpMTU := maxPacketSize - message.headerSize()
|
||||||
|
for remaining := len(originPacket); remaining > 0; remaining -= udpMTU {
|
||||||
|
fragment := allocMessage()
|
||||||
|
*fragment = *message
|
||||||
|
if remaining > udpMTU {
|
||||||
|
fragment.data = buf.As(originPacket[:udpMTU])
|
||||||
|
originPacket = originPacket[udpMTU:]
|
||||||
|
} else {
|
||||||
|
fragment.data = buf.As(originPacket)
|
||||||
|
originPacket = nil
|
||||||
|
}
|
||||||
|
fragments = append(fragments, fragment)
|
||||||
|
}
|
||||||
|
fragmentTotal := uint16(len(fragments))
|
||||||
|
for index, fragment := range fragments {
|
||||||
|
fragment.fragmentID = uint8(index)
|
||||||
|
fragment.fragmentTotal = uint8(fragmentTotal)
|
||||||
|
/*if index > 0 {
|
||||||
|
fragment.destination = ""
|
||||||
|
// not work in hysteria
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
return fragments
|
||||||
|
}
|
||||||
|
|
||||||
|
type udpPacketConn struct {
|
||||||
|
ctx context.Context
|
||||||
|
cancel common.ContextCancelCauseFunc
|
||||||
|
sessionID uint32
|
||||||
|
quicConn quic.Connection
|
||||||
|
data chan *udpMessage
|
||||||
|
udpMTU int
|
||||||
|
udpMTUTime time.Time
|
||||||
|
packetId atomic.Uint32
|
||||||
|
closeOnce sync.Once
|
||||||
|
defragger *udpDefragger
|
||||||
|
onDestroy func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUDPPacketConn(ctx context.Context, quicConn quic.Connection, onDestroy func()) *udpPacketConn {
|
||||||
|
ctx, cancel := common.ContextWithCancelCause(ctx)
|
||||||
|
return &udpPacketConn{
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
quicConn: quicConn,
|
||||||
|
data: make(chan *udpMessage, 64),
|
||||||
|
defragger: newUDPDefragger(),
|
||||||
|
onDestroy: onDestroy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *udpPacketConn) ReadPacketThreadSafe() (buffer *buf.Buffer, destination M.Socksaddr, err error) {
|
||||||
|
select {
|
||||||
|
case p := <-c.data:
|
||||||
|
buffer = p.data
|
||||||
|
destination = M.ParseSocksaddr(p.destination)
|
||||||
|
p.release()
|
||||||
|
return
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return nil, M.Socksaddr{}, io.ErrClosedPipe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *udpPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
|
||||||
|
select {
|
||||||
|
case p := <-c.data:
|
||||||
|
_, err = buffer.ReadOnceFrom(p.data)
|
||||||
|
destination = M.ParseSocksaddr(p.destination)
|
||||||
|
p.releaseMessage()
|
||||||
|
return
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return M.Socksaddr{}, io.ErrClosedPipe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *udpPacketConn) WaitReadPacket(newBuffer func() *buf.Buffer) (destination M.Socksaddr, err error) {
|
||||||
|
select {
|
||||||
|
case p := <-c.data:
|
||||||
|
_, err = newBuffer().ReadOnceFrom(p.data)
|
||||||
|
destination = M.ParseSocksaddr(p.destination)
|
||||||
|
p.releaseMessage()
|
||||||
|
return
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return M.Socksaddr{}, io.ErrClosedPipe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *udpPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||||
|
select {
|
||||||
|
case pkt := <-c.data:
|
||||||
|
n = copy(p, pkt.data.Bytes())
|
||||||
|
destination := M.ParseSocksaddr(pkt.destination)
|
||||||
|
if destination.IsFqdn() {
|
||||||
|
addr = destination
|
||||||
|
} else {
|
||||||
|
addr = destination.UDPAddr()
|
||||||
|
}
|
||||||
|
pkt.releaseMessage()
|
||||||
|
return n, addr, nil
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return 0, nil, io.ErrClosedPipe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *udpPacketConn) needFragment() bool {
|
||||||
|
nowTime := time.Now()
|
||||||
|
if c.udpMTU > 0 && nowTime.Sub(c.udpMTUTime) < 5*time.Second {
|
||||||
|
c.udpMTUTime = nowTime
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *udpPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||||
|
defer buffer.Release()
|
||||||
|
select {
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return net.ErrClosed
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
if buffer.Len() > 0xffff {
|
||||||
|
return quic.ErrMessageTooLarge(0xffff)
|
||||||
|
}
|
||||||
|
packetId := c.packetId.Add(1)
|
||||||
|
if packetId > math.MaxUint16 {
|
||||||
|
c.packetId.Store(0)
|
||||||
|
packetId = 0
|
||||||
|
}
|
||||||
|
message := allocMessage()
|
||||||
|
*message = udpMessage{
|
||||||
|
sessionID: c.sessionID,
|
||||||
|
packetID: uint16(packetId),
|
||||||
|
fragmentTotal: 1,
|
||||||
|
destination: destination.String(),
|
||||||
|
data: buffer,
|
||||||
|
}
|
||||||
|
defer message.releaseMessage()
|
||||||
|
var err error
|
||||||
|
if c.needFragment() && buffer.Len() > c.udpMTU {
|
||||||
|
err = c.writePackets(fragUDPMessage(message, c.udpMTU))
|
||||||
|
} else {
|
||||||
|
err = c.writePacket(message)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var tooLargeErr quic.ErrMessageTooLarge
|
||||||
|
if !errors.As(err, &tooLargeErr) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.udpMTU = int(tooLargeErr)
|
||||||
|
c.udpMTUTime = time.Now()
|
||||||
|
return c.writePackets(fragUDPMessage(message, c.udpMTU))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *udpPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||||
|
select {
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return 0, net.ErrClosed
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
if len(p) > 0xffff {
|
||||||
|
return 0, quic.ErrMessageTooLarge(0xffff)
|
||||||
|
}
|
||||||
|
packetId := c.packetId.Add(1)
|
||||||
|
if packetId > math.MaxUint16 {
|
||||||
|
c.packetId.Store(0)
|
||||||
|
packetId = 0
|
||||||
|
}
|
||||||
|
message := allocMessage()
|
||||||
|
*message = udpMessage{
|
||||||
|
sessionID: c.sessionID,
|
||||||
|
packetID: uint16(packetId),
|
||||||
|
fragmentTotal: 1,
|
||||||
|
destination: addr.String(),
|
||||||
|
data: buf.As(p),
|
||||||
|
}
|
||||||
|
if c.needFragment() && len(p) > c.udpMTU {
|
||||||
|
err = c.writePackets(fragUDPMessage(message, c.udpMTU))
|
||||||
|
if err == nil {
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = c.writePacket(message)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
var tooLargeErr quic.ErrMessageTooLarge
|
||||||
|
if !errors.As(err, &tooLargeErr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.udpMTU = int(tooLargeErr)
|
||||||
|
c.udpMTUTime = time.Now()
|
||||||
|
err = c.writePackets(fragUDPMessage(message, c.udpMTU))
|
||||||
|
if err == nil {
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *udpPacketConn) inputPacket(message *udpMessage) {
|
||||||
|
if message.fragmentTotal <= 1 {
|
||||||
|
select {
|
||||||
|
case c.data <- message:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newMessage := c.defragger.feed(message)
|
||||||
|
if newMessage != nil {
|
||||||
|
select {
|
||||||
|
case c.data <- newMessage:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *udpPacketConn) writePackets(messages []*udpMessage) error {
|
||||||
|
defer releaseMessages(messages)
|
||||||
|
for _, message := range messages {
|
||||||
|
err := c.writePacket(message)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *udpPacketConn) writePacket(message *udpMessage) error {
|
||||||
|
buffer := message.pack()
|
||||||
|
defer buffer.Release()
|
||||||
|
return c.quicConn.SendMessage(buffer.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *udpPacketConn) Close() error {
|
||||||
|
c.closeOnce.Do(func() {
|
||||||
|
c.closeWithError(os.ErrClosed)
|
||||||
|
c.onDestroy()
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *udpPacketConn) closeWithError(err error) {
|
||||||
|
c.cancel(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *udpPacketConn) LocalAddr() net.Addr {
|
||||||
|
return c.quicConn.LocalAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *udpPacketConn) SetDeadline(t time.Time) error {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *udpPacketConn) SetReadDeadline(t time.Time) error {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *udpPacketConn) SetWriteDeadline(t time.Time) error {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
type udpDefragger struct {
|
||||||
|
packetMap *cache.LruCache[uint16, *packetItem]
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUDPDefragger() *udpDefragger {
|
||||||
|
return &udpDefragger{
|
||||||
|
packetMap: cache.New(
|
||||||
|
cache.WithAge[uint16, *packetItem](10),
|
||||||
|
cache.WithUpdateAgeOnGet[uint16, *packetItem](),
|
||||||
|
cache.WithEvict[uint16, *packetItem](func(key uint16, value *packetItem) {
|
||||||
|
releaseMessages(value.messages)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type packetItem struct {
|
||||||
|
access sync.Mutex
|
||||||
|
messages []*udpMessage
|
||||||
|
count uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *udpDefragger) feed(m *udpMessage) *udpMessage {
|
||||||
|
if m.fragmentTotal <= 1 {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
if m.fragmentID >= m.fragmentTotal {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
item, _ := d.packetMap.LoadOrStore(m.packetID, newPacketItem)
|
||||||
|
item.access.Lock()
|
||||||
|
defer item.access.Unlock()
|
||||||
|
if int(m.fragmentTotal) != len(item.messages) {
|
||||||
|
releaseMessages(item.messages)
|
||||||
|
item.messages = make([]*udpMessage, m.fragmentTotal)
|
||||||
|
item.count = 1
|
||||||
|
item.messages[m.fragmentID] = m
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if item.messages[m.fragmentID] != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
item.messages[m.fragmentID] = m
|
||||||
|
item.count++
|
||||||
|
if int(item.count) != len(item.messages) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
newMessage := allocMessage()
|
||||||
|
newMessage.sessionID = m.sessionID
|
||||||
|
newMessage.packetID = m.packetID
|
||||||
|
newMessage.destination = item.messages[0].destination
|
||||||
|
var finalLength int
|
||||||
|
for _, message := range item.messages {
|
||||||
|
finalLength += message.data.Len()
|
||||||
|
}
|
||||||
|
if finalLength > 0 {
|
||||||
|
newMessage.data = buf.NewSize(finalLength)
|
||||||
|
for _, message := range item.messages {
|
||||||
|
newMessage.data.Write(message.data.Bytes())
|
||||||
|
message.releaseMessage()
|
||||||
|
}
|
||||||
|
item.messages = nil
|
||||||
|
return newMessage
|
||||||
|
}
|
||||||
|
item.messages = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPacketItem() *packetItem {
|
||||||
|
return new(packetItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeUDPMessage(message *udpMessage, data []byte) error {
|
||||||
|
reader := bytes.NewReader(data)
|
||||||
|
err := binary.Read(reader, binary.BigEndian, &message.sessionID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &message.packetID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &message.fragmentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &message.fragmentTotal)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
message.destination, err = protocol.ReadVString(reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
message.data = buf.As(data[len(data)-reader.Len():])
|
||||||
|
return nil
|
||||||
|
}
|
106
transport/hysteria2/salamander.go
Normal file
106
transport/hysteria2/salamander.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package hysteria2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/buf"
|
||||||
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/blake2b"
|
||||||
|
)
|
||||||
|
|
||||||
|
const salamanderSaltLen = 8
|
||||||
|
|
||||||
|
const ObfsTypeSalamander = "salamander"
|
||||||
|
|
||||||
|
type Salamander struct {
|
||||||
|
net.PacketConn
|
||||||
|
password []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSalamanderConn(conn net.PacketConn, password []byte) net.PacketConn {
|
||||||
|
writer, isVectorised := bufio.CreateVectorisedPacketWriter(conn)
|
||||||
|
if isVectorised {
|
||||||
|
return &VectorisedSalamander{
|
||||||
|
Salamander: Salamander{
|
||||||
|
PacketConn: conn,
|
||||||
|
password: password,
|
||||||
|
},
|
||||||
|
writer: writer,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return &Salamander{
|
||||||
|
PacketConn: conn,
|
||||||
|
password: password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Salamander) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||||
|
n, addr, err = s.PacketConn.ReadFrom(p)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if n <= salamanderSaltLen {
|
||||||
|
return 0, nil, E.New("salamander: packet too short")
|
||||||
|
}
|
||||||
|
key := blake2b.Sum256(append(s.password, p[:salamanderSaltLen]...))
|
||||||
|
for index, c := range p[salamanderSaltLen:n] {
|
||||||
|
p[index] = c ^ key[index%blake2b.Size256]
|
||||||
|
}
|
||||||
|
return n - salamanderSaltLen, addr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Salamander) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||||
|
buffer := buf.NewSize(len(p) + salamanderSaltLen)
|
||||||
|
defer buffer.Release()
|
||||||
|
buffer.WriteRandom(salamanderSaltLen)
|
||||||
|
key := blake2b.Sum256(append(s.password, buffer.Bytes()...))
|
||||||
|
for index, c := range p {
|
||||||
|
common.Must(buffer.WriteByte(c ^ key[index%blake2b.Size256]))
|
||||||
|
}
|
||||||
|
_, err = s.PacketConn.WriteTo(buffer.Bytes(), addr)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type VectorisedSalamander struct {
|
||||||
|
Salamander
|
||||||
|
writer N.VectorisedPacketWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *VectorisedSalamander) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||||
|
buffer := buf.NewSize(salamanderSaltLen)
|
||||||
|
buffer.WriteRandom(salamanderSaltLen)
|
||||||
|
key := blake2b.Sum256(append(s.password, buffer.Bytes()...))
|
||||||
|
for i := range p {
|
||||||
|
p[i] ^= key[i%blake2b.Size256]
|
||||||
|
}
|
||||||
|
err = s.writer.WriteVectorisedPacket([]*buf.Buffer{buffer, buf.As(p)}, M.SocksaddrFromNet(addr))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *VectorisedSalamander) WriteVectorisedPacket(buffers []*buf.Buffer, destination M.Socksaddr) error {
|
||||||
|
header := buf.NewSize(salamanderSaltLen)
|
||||||
|
defer header.Release()
|
||||||
|
header.WriteRandom(salamanderSaltLen)
|
||||||
|
key := blake2b.Sum256(append(s.password, header.Bytes()...))
|
||||||
|
var bufferIndex int
|
||||||
|
for _, buffer := range buffers {
|
||||||
|
content := buffer.Bytes()
|
||||||
|
for index, c := range content {
|
||||||
|
content[bufferIndex+index] = c ^ key[bufferIndex+index%blake2b.Size256]
|
||||||
|
}
|
||||||
|
bufferIndex += len(content)
|
||||||
|
}
|
||||||
|
return s.writer.WriteVectorisedPacket(append([]*buf.Buffer{header}, buffers...), destination)
|
||||||
|
}
|
335
transport/hysteria2/server.go
Normal file
335
transport/hysteria2/server.go
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
package hysteria2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/quic-go"
|
||||||
|
"github.com/sagernet/quic-go/http3"
|
||||||
|
"github.com/sagernet/sing-box/common/qtls"
|
||||||
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
|
"github.com/sagernet/sing-box/transport/hysteria2/congestion"
|
||||||
|
"github.com/sagernet/sing-box/transport/hysteria2/internal/protocol"
|
||||||
|
tuicCongestion "github.com/sagernet/sing-box/transport/tuic/congestion"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/auth"
|
||||||
|
"github.com/sagernet/sing/common/baderror"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServerOptions struct {
|
||||||
|
Context context.Context
|
||||||
|
Logger logger.Logger
|
||||||
|
SendBPS uint64
|
||||||
|
ReceiveBPS uint64
|
||||||
|
IgnoreClientBandwidth bool
|
||||||
|
SalamanderPassword string
|
||||||
|
TLSConfig tls.ServerConfig
|
||||||
|
Users []User
|
||||||
|
UDPDisabled bool
|
||||||
|
Handler ServerHandler
|
||||||
|
MasqueradeHandler http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Name string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerHandler interface {
|
||||||
|
N.TCPConnectionHandler
|
||||||
|
N.UDPConnectionHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
ctx context.Context
|
||||||
|
logger logger.Logger
|
||||||
|
sendBPS uint64
|
||||||
|
receiveBPS uint64
|
||||||
|
ignoreClientBandwidth bool
|
||||||
|
salamanderPassword string
|
||||||
|
tlsConfig tls.ServerConfig
|
||||||
|
quicConfig *quic.Config
|
||||||
|
userMap map[string]User
|
||||||
|
udpDisabled bool
|
||||||
|
handler ServerHandler
|
||||||
|
masqueradeHandler http.Handler
|
||||||
|
quicListener io.Closer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(options ServerOptions) (*Server, error) {
|
||||||
|
quicConfig := &quic.Config{
|
||||||
|
DisablePathMTUDiscovery: !(runtime.GOOS == "windows" || runtime.GOOS == "linux" || runtime.GOOS == "android" || runtime.GOOS == "darwin"),
|
||||||
|
MaxDatagramFrameSize: 1400,
|
||||||
|
EnableDatagrams: !options.UDPDisabled,
|
||||||
|
MaxIncomingStreams: 1 << 60,
|
||||||
|
InitialStreamReceiveWindow: defaultStreamReceiveWindow,
|
||||||
|
MaxStreamReceiveWindow: defaultStreamReceiveWindow,
|
||||||
|
InitialConnectionReceiveWindow: defaultConnReceiveWindow,
|
||||||
|
MaxConnectionReceiveWindow: defaultConnReceiveWindow,
|
||||||
|
MaxIdleTimeout: defaultMaxIdleTimeout,
|
||||||
|
KeepAlivePeriod: defaultKeepAlivePeriod,
|
||||||
|
}
|
||||||
|
if len(options.Users) == 0 {
|
||||||
|
return nil, E.New("missing users")
|
||||||
|
}
|
||||||
|
userMap := make(map[string]User)
|
||||||
|
for _, user := range options.Users {
|
||||||
|
userMap[user.Password] = user
|
||||||
|
}
|
||||||
|
if options.MasqueradeHandler == nil {
|
||||||
|
options.MasqueradeHandler = http.NotFoundHandler()
|
||||||
|
}
|
||||||
|
return &Server{
|
||||||
|
ctx: options.Context,
|
||||||
|
logger: options.Logger,
|
||||||
|
sendBPS: options.SendBPS,
|
||||||
|
receiveBPS: options.ReceiveBPS,
|
||||||
|
ignoreClientBandwidth: options.IgnoreClientBandwidth,
|
||||||
|
salamanderPassword: options.SalamanderPassword,
|
||||||
|
tlsConfig: options.TLSConfig,
|
||||||
|
quicConfig: quicConfig,
|
||||||
|
userMap: userMap,
|
||||||
|
udpDisabled: options.UDPDisabled,
|
||||||
|
handler: options.Handler,
|
||||||
|
masqueradeHandler: options.MasqueradeHandler,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Start(conn net.PacketConn) error {
|
||||||
|
if s.salamanderPassword != "" {
|
||||||
|
conn = NewSalamanderConn(conn, []byte(s.salamanderPassword))
|
||||||
|
}
|
||||||
|
err := qtls.ConfigureHTTP3(s.tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
listener, err := qtls.Listen(conn, s.tlsConfig, s.quicConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.quicListener = listener
|
||||||
|
go s.loopConnections(listener)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Close() error {
|
||||||
|
return common.Close(
|
||||||
|
s.quicListener,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) loopConnections(listener qtls.QUICListener) {
|
||||||
|
for {
|
||||||
|
connection, err := listener.Accept(s.ctx)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "server closed") {
|
||||||
|
s.logger.Debug(E.Cause(err, "listener closed"))
|
||||||
|
} else {
|
||||||
|
s.logger.Error(E.Cause(err, "listener closed"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go s.handleConnection(connection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleConnection(connection quic.Connection) {
|
||||||
|
session := &serverSession{
|
||||||
|
Server: s,
|
||||||
|
ctx: s.ctx,
|
||||||
|
quicConn: connection,
|
||||||
|
source: M.SocksaddrFromNet(connection.RemoteAddr()),
|
||||||
|
connDone: make(chan struct{}),
|
||||||
|
udpConnMap: make(map[uint32]*udpPacketConn),
|
||||||
|
}
|
||||||
|
httpServer := http3.Server{
|
||||||
|
Handler: session,
|
||||||
|
StreamHijacker: session.handleStream0,
|
||||||
|
}
|
||||||
|
_ = httpServer.ServeQUICConn(connection)
|
||||||
|
_ = connection.CloseWithError(0, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverSession struct {
|
||||||
|
*Server
|
||||||
|
ctx context.Context
|
||||||
|
quicConn quic.Connection
|
||||||
|
source M.Socksaddr
|
||||||
|
connAccess sync.Mutex
|
||||||
|
connDone chan struct{}
|
||||||
|
connErr error
|
||||||
|
authenticated bool
|
||||||
|
authUser *User
|
||||||
|
udpAccess sync.RWMutex
|
||||||
|
udpConnMap map[uint32]*udpPacketConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serverSession) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == http.MethodPost && r.Host == protocol.URLHost && r.URL.Path == protocol.URLPath {
|
||||||
|
if s.authenticated {
|
||||||
|
protocol.AuthResponseToHeader(w.Header(), protocol.AuthResponse{
|
||||||
|
UDPEnabled: !s.udpDisabled,
|
||||||
|
Rx: s.receiveBPS,
|
||||||
|
RxAuto: s.ignoreClientBandwidth,
|
||||||
|
})
|
||||||
|
w.WriteHeader(protocol.StatusAuthOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
request := protocol.AuthRequestFromHeader(r.Header)
|
||||||
|
user, loaded := s.userMap[request.Auth]
|
||||||
|
if !loaded {
|
||||||
|
s.masqueradeHandler.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.authUser = &user
|
||||||
|
s.authenticated = true
|
||||||
|
if !s.ignoreClientBandwidth && request.Rx > 0 {
|
||||||
|
var sendBps uint64
|
||||||
|
if s.sendBPS > 0 && s.sendBPS < request.Rx {
|
||||||
|
sendBps = s.sendBPS
|
||||||
|
} else {
|
||||||
|
sendBps = request.Rx
|
||||||
|
}
|
||||||
|
s.quicConn.SetCongestionControl(congestion.NewBrutalSender(sendBps))
|
||||||
|
} else {
|
||||||
|
s.quicConn.SetCongestionControl(tuicCongestion.NewBBRSender(
|
||||||
|
tuicCongestion.DefaultClock{},
|
||||||
|
tuicCongestion.GetInitialPacketSize(s.quicConn.RemoteAddr()),
|
||||||
|
tuicCongestion.InitialCongestionWindow*tuicCongestion.InitialMaxDatagramSize,
|
||||||
|
tuicCongestion.DefaultBBRMaxCongestionWindow*tuicCongestion.InitialMaxDatagramSize,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
protocol.AuthResponseToHeader(w.Header(), protocol.AuthResponse{
|
||||||
|
UDPEnabled: !s.udpDisabled,
|
||||||
|
Rx: s.receiveBPS,
|
||||||
|
RxAuto: s.ignoreClientBandwidth,
|
||||||
|
})
|
||||||
|
w.WriteHeader(protocol.StatusAuthOK)
|
||||||
|
if s.ctx.Done() != nil {
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
s.closeWithError(s.ctx.Err())
|
||||||
|
case <-s.connDone:
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
if !s.udpDisabled {
|
||||||
|
go s.loopMessages()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.masqueradeHandler.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serverSession) handleStream0(frameType http3.FrameType, connection quic.Connection, stream quic.Stream, err error) (bool, error) {
|
||||||
|
if !s.authenticated || err != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if frameType != protocol.FrameTypeTCPRequest {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
hErr := s.handleStream(stream)
|
||||||
|
stream.CancelRead(0)
|
||||||
|
stream.Close()
|
||||||
|
if hErr != nil {
|
||||||
|
stream.CancelRead(0)
|
||||||
|
stream.Close()
|
||||||
|
s.logger.Error(E.Cause(hErr, "handle stream request"))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serverSession) handleStream(stream quic.Stream) error {
|
||||||
|
destinationString, err := protocol.ReadTCPRequest(stream)
|
||||||
|
if err != nil {
|
||||||
|
return E.New("read TCP request")
|
||||||
|
}
|
||||||
|
ctx := s.ctx
|
||||||
|
if s.authUser.Name != "" {
|
||||||
|
ctx = auth.ContextWithUser(s.ctx, s.authUser.Name)
|
||||||
|
}
|
||||||
|
_ = s.handler.NewConnection(ctx, &serverConn{Stream: stream}, M.Metadata{
|
||||||
|
Source: s.source,
|
||||||
|
Destination: M.ParseSocksaddr(destinationString),
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serverSession) closeWithError(err error) {
|
||||||
|
s.connAccess.Lock()
|
||||||
|
defer s.connAccess.Unlock()
|
||||||
|
select {
|
||||||
|
case <-s.connDone:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
s.connErr = err
|
||||||
|
close(s.connDone)
|
||||||
|
}
|
||||||
|
if E.IsClosedOrCanceled(err) {
|
||||||
|
s.logger.Debug(E.Cause(err, "connection failed"))
|
||||||
|
} else {
|
||||||
|
s.logger.Error(E.Cause(err, "connection failed"))
|
||||||
|
}
|
||||||
|
_ = s.quicConn.CloseWithError(0, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConn struct {
|
||||||
|
quic.Stream
|
||||||
|
responseWritten bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *serverConn) HandshakeFailure(err error) error {
|
||||||
|
if c.responseWritten {
|
||||||
|
return os.ErrClosed
|
||||||
|
}
|
||||||
|
c.responseWritten = true
|
||||||
|
buffer := protocol.WriteTCPResponse(false, err.Error(), nil)
|
||||||
|
defer buffer.Release()
|
||||||
|
return common.Error(c.Stream.Write(buffer.Bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *serverConn) Read(p []byte) (n int, err error) {
|
||||||
|
n, err = c.Stream.Read(p)
|
||||||
|
return n, baderror.WrapQUIC(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *serverConn) Write(p []byte) (n int, err error) {
|
||||||
|
if !c.responseWritten {
|
||||||
|
c.responseWritten = true
|
||||||
|
buffer := protocol.WriteTCPResponse(true, "", p)
|
||||||
|
defer buffer.Release()
|
||||||
|
_, err = c.Stream.Write(buffer.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return 0, baderror.WrapQUIC(err)
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
n, err = c.Stream.Write(p)
|
||||||
|
return n, baderror.WrapQUIC(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *serverConn) LocalAddr() net.Addr {
|
||||||
|
return M.Socksaddr{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *serverConn) RemoteAddr() net.Addr {
|
||||||
|
return M.Socksaddr{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *serverConn) Close() error {
|
||||||
|
c.Stream.CancelRead(0)
|
||||||
|
return c.Stream.Close()
|
||||||
|
}
|
55
transport/hysteria2/server_packet.go
Normal file
55
transport/hysteria2/server_packet.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package hysteria2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *serverSession) loopMessages() {
|
||||||
|
for {
|
||||||
|
message, err := s.quicConn.ReceiveMessage(s.ctx)
|
||||||
|
if err != nil {
|
||||||
|
s.closeWithError(E.Cause(err, "receive message"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hErr := s.handleMessage(message)
|
||||||
|
if hErr != nil {
|
||||||
|
s.closeWithError(E.Cause(hErr, "handle message"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serverSession) handleMessage(data []byte) error {
|
||||||
|
message := allocMessage()
|
||||||
|
err := decodeUDPMessage(message, data)
|
||||||
|
if err != nil {
|
||||||
|
message.release()
|
||||||
|
return E.Cause(err, "decode UDP message")
|
||||||
|
}
|
||||||
|
s.handleUDPMessage(message)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serverSession) handleUDPMessage(message *udpMessage) {
|
||||||
|
s.udpAccess.RLock()
|
||||||
|
udpConn, loaded := s.udpConnMap[message.sessionID]
|
||||||
|
s.udpAccess.RUnlock()
|
||||||
|
if !loaded || common.Done(udpConn.ctx) {
|
||||||
|
udpConn = newUDPPacketConn(s.ctx, s.quicConn, func() {
|
||||||
|
s.udpAccess.Lock()
|
||||||
|
delete(s.udpConnMap, message.sessionID)
|
||||||
|
s.udpAccess.Unlock()
|
||||||
|
})
|
||||||
|
udpConn.sessionID = message.sessionID
|
||||||
|
s.udpAccess.Lock()
|
||||||
|
s.udpConnMap[message.sessionID] = udpConn
|
||||||
|
s.udpAccess.Unlock()
|
||||||
|
go s.handler.NewPacketConnection(udpConn.ctx, udpConn, M.Metadata{
|
||||||
|
Source: s.source,
|
||||||
|
Destination: M.ParseSocksaddr(message.destination),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
udpConn.inputPacket(message)
|
||||||
|
}
|
@ -18,7 +18,7 @@ func init() {
|
|||||||
RegisterPlugin("obfs-local", newObfsLocal)
|
RegisterPlugin("obfs-local", newObfsLocal)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newObfsLocal(pluginOpts Args, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error) {
|
func newObfsLocal(ctx context.Context, pluginOpts Args, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error) {
|
||||||
plugin := &ObfsLocal{
|
plugin := &ObfsLocal{
|
||||||
dialer: dialer,
|
dialer: dialer,
|
||||||
serverAddr: serverAddr,
|
serverAddr: serverAddr,
|
||||||
|
@ -10,7 +10,7 @@ import (
|
|||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PluginConstructor func(pluginArgs Args, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error)
|
type PluginConstructor func(ctx context.Context, pluginArgs Args, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error)
|
||||||
|
|
||||||
type Plugin interface {
|
type Plugin interface {
|
||||||
DialContext(ctx context.Context) (net.Conn, error)
|
DialContext(ctx context.Context) (net.Conn, error)
|
||||||
@ -25,7 +25,7 @@ func RegisterPlugin(name string, constructor PluginConstructor) {
|
|||||||
plugins[name] = constructor
|
plugins[name] = constructor
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreatePlugin(name string, pluginArgs string, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error) {
|
func CreatePlugin(ctx context.Context, name string, pluginArgs string, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error) {
|
||||||
pluginOptions, err := ParsePluginOptions(pluginArgs)
|
pluginOptions, err := ParsePluginOptions(pluginArgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "parse plugin_opts")
|
return nil, E.Cause(err, "parse plugin_opts")
|
||||||
@ -34,5 +34,5 @@ func CreatePlugin(name string, pluginArgs string, router adapter.Router, dialer
|
|||||||
if !loaded {
|
if !loaded {
|
||||||
return nil, E.New("plugin not found: ", name)
|
return nil, E.New("plugin not found: ", name)
|
||||||
}
|
}
|
||||||
return constructor(pluginOptions, router, dialer, serverAddr)
|
return constructor(ctx, pluginOptions, router, dialer, serverAddr)
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ func init() {
|
|||||||
RegisterPlugin("v2ray-plugin", newV2RayPlugin)
|
RegisterPlugin("v2ray-plugin", newV2RayPlugin)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newV2RayPlugin(pluginOpts Args, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error) {
|
func newV2RayPlugin(ctx context.Context, pluginOpts Args, router adapter.Router, dialer N.Dialer, serverAddr M.Socksaddr) (Plugin, error) {
|
||||||
var tlsOptions option.OutboundTLSOptions
|
var tlsOptions option.OutboundTLSOptions
|
||||||
if _, loaded := pluginOpts.Get("tls"); loaded {
|
if _, loaded := pluginOpts.Get("tls"); loaded {
|
||||||
tlsOptions.Enabled = true
|
tlsOptions.Enabled = true
|
||||||
@ -54,7 +54,7 @@ func newV2RayPlugin(pluginOpts Args, router adapter.Router, dialer N.Dialer, ser
|
|||||||
var tlsClient tls.Config
|
var tlsClient tls.Config
|
||||||
var err error
|
var err error
|
||||||
if tlsOptions.Enabled {
|
if tlsOptions.Enabled {
|
||||||
tlsClient, err = tls.NewClient(router, serverAddr.AddrString(), tlsOptions)
|
tlsClient, err = tls.NewClient(ctx, serverAddr.AddrString(), tlsOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
//go:build with_quic
|
||||||
|
|
||||||
package tuic
|
package tuic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"runtime"
|
"runtime"
|
||||||
@ -10,6 +11,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/quic-go"
|
"github.com/sagernet/quic-go"
|
||||||
|
"github.com/sagernet/sing-box/common/qtls"
|
||||||
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/baderror"
|
"github.com/sagernet/sing/common/baderror"
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/sagernet/sing/common/buf"
|
||||||
@ -25,7 +28,7 @@ type ClientOptions struct {
|
|||||||
Context context.Context
|
Context context.Context
|
||||||
Dialer N.Dialer
|
Dialer N.Dialer
|
||||||
ServerAddress M.Socksaddr
|
ServerAddress M.Socksaddr
|
||||||
TLSConfig *tls.Config
|
TLSConfig tls.Config
|
||||||
UUID uuid.UUID
|
UUID uuid.UUID
|
||||||
Password string
|
Password string
|
||||||
CongestionControl string
|
CongestionControl string
|
||||||
@ -38,7 +41,7 @@ type Client struct {
|
|||||||
ctx context.Context
|
ctx context.Context
|
||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
serverAddr M.Socksaddr
|
serverAddr M.Socksaddr
|
||||||
tlsConfig *tls.Config
|
tlsConfig tls.Config
|
||||||
quicConfig *quic.Config
|
quicConfig *quic.Config
|
||||||
uuid uuid.UUID
|
uuid uuid.UUID
|
||||||
password string
|
password string
|
||||||
@ -108,9 +111,9 @@ func (c *Client) offerNew(ctx context.Context) (*clientQUICConnection, error) {
|
|||||||
}
|
}
|
||||||
var quicConn quic.Connection
|
var quicConn quic.Connection
|
||||||
if c.zeroRTTHandshake {
|
if c.zeroRTTHandshake {
|
||||||
quicConn, err = quic.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), c.tlsConfig, c.quicConfig)
|
quicConn, err = qtls.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), c.tlsConfig, c.quicConfig)
|
||||||
} else {
|
} else {
|
||||||
quicConn, err = quic.Dial(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), c.tlsConfig, c.quicConfig)
|
quicConn, err = qtls.Dial(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), c.tlsConfig, c.quicConfig)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
udpConn.Close()
|
udpConn.Close()
|
||||||
@ -141,13 +144,13 @@ func (c *Client) offerNew(ctx context.Context) (*clientQUICConnection, error) {
|
|||||||
func (c *Client) clientHandshake(conn quic.Connection) error {
|
func (c *Client) clientHandshake(conn quic.Connection) error {
|
||||||
authStream, err := conn.OpenUniStream()
|
authStream, err := conn.OpenUniStream()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return E.Cause(err, "open handshake stream")
|
||||||
}
|
}
|
||||||
defer authStream.Close()
|
defer authStream.Close()
|
||||||
handshakeState := conn.ConnectionState().TLS
|
handshakeState := conn.ConnectionState()
|
||||||
tuicAuthToken, err := handshakeState.ExportKeyingMaterial(string(c.uuid[:]), []byte(c.password), 32)
|
tuicAuthToken, err := handshakeState.ExportKeyingMaterial(string(c.uuid[:]), []byte(c.password), 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return E.Cause(err, "export keying material")
|
||||||
}
|
}
|
||||||
authRequest := buf.NewSize(AuthenticateLen)
|
authRequest := buf.NewSize(AuthenticateLen)
|
||||||
authRequest.WriteByte(Version)
|
authRequest.WriteByte(Version)
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//go:build with_quic
|
||||||
|
|
||||||
package tuic
|
package tuic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
|
//go:build with_quic
|
||||||
|
|
||||||
package tuic
|
package tuic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
@ -13,6 +14,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/quic-go"
|
"github.com/sagernet/quic-go"
|
||||||
|
"github.com/sagernet/sing-box/common/qtls"
|
||||||
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/auth"
|
"github.com/sagernet/sing/common/auth"
|
||||||
"github.com/sagernet/sing/common/baderror"
|
"github.com/sagernet/sing/common/baderror"
|
||||||
@ -29,7 +32,7 @@ import (
|
|||||||
type ServerOptions struct {
|
type ServerOptions struct {
|
||||||
Context context.Context
|
Context context.Context
|
||||||
Logger logger.Logger
|
Logger logger.Logger
|
||||||
TLSConfig *tls.Config
|
TLSConfig tls.ServerConfig
|
||||||
Users []User
|
Users []User
|
||||||
CongestionControl string
|
CongestionControl string
|
||||||
AuthTimeout time.Duration
|
AuthTimeout time.Duration
|
||||||
@ -52,7 +55,7 @@ type ServerHandler interface {
|
|||||||
type Server struct {
|
type Server struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
tlsConfig *tls.Config
|
tlsConfig tls.ServerConfig
|
||||||
heartbeat time.Duration
|
heartbeat time.Duration
|
||||||
quicConfig *quic.Config
|
quicConfig *quic.Config
|
||||||
userMap map[uuid.UUID]User
|
userMap map[uuid.UUID]User
|
||||||
@ -107,7 +110,7 @@ func NewServer(options ServerOptions) (*Server, error) {
|
|||||||
|
|
||||||
func (s *Server) Start(conn net.PacketConn) error {
|
func (s *Server) Start(conn net.PacketConn) error {
|
||||||
if !s.quicConfig.Allow0RTT {
|
if !s.quicConfig.Allow0RTT {
|
||||||
listener, err := quic.Listen(conn, s.tlsConfig, s.quicConfig)
|
listener, err := qtls.Listen(conn, s.tlsConfig, s.quicConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -127,7 +130,7 @@ func (s *Server) Start(conn net.PacketConn) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
} else {
|
} else {
|
||||||
listener, err := quic.ListenEarly(conn, s.tlsConfig, s.quicConfig)
|
listener, err := qtls.ListenEarly(conn, s.tlsConfig, s.quicConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -247,7 +250,7 @@ func (s *serverSession) handleUniStream(stream quic.ReceiveStream) error {
|
|||||||
if !loaded {
|
if !loaded {
|
||||||
return E.New("authentication: unknown user ", userUUID)
|
return E.New("authentication: unknown user ", userUUID)
|
||||||
}
|
}
|
||||||
handshakeState := s.quicConn.ConnectionState().TLS
|
handshakeState := s.quicConn.ConnectionState()
|
||||||
tuicToken, err := handshakeState.ExportKeyingMaterial(string(user.UUID[:]), []byte(user.Password), 32)
|
tuicToken, err := handshakeState.ExportKeyingMaterial(string(user.UUID[:]), []byte(user.Password), 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "authentication: export keying material")
|
return E.Cause(err, "authentication: export keying material")
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//go:build with_quic
|
||||||
|
|
||||||
package tuic
|
package tuic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user