ソースを参照

Add AnyTLS protocol

anytls 8 ヶ月 前
コミット
3ea305b882

+ 1 - 0
constant/proxy.go

@@ -19,6 +19,7 @@ const (
 	TypeTor          = "tor"
 	TypeSSH          = "ssh"
 	TypeShadowTLS    = "shadowtls"
+	TypeAnyTLS       = "anytls"
 	TypeShadowsocksR = "shadowsocksr"
 	TypeVLESS        = "vless"
 	TypeTUIC         = "tuic"

+ 39 - 0
docs/configuration/inbound/anytls.md

@@ -0,0 +1,39 @@
+### Structure
+
+```json
+{
+  "type": "anytls",
+  "tag": "anytls-in",
+
+  ... // Listen Fields
+
+  "users": [
+    {
+      "name": "sekai",
+      "password": "8JCsPssfgS8tiRwiMlhARg=="
+    }
+  ],
+  "padding_scheme": [],
+  "tls": {}
+}
+```
+
+### Listen Fields
+
+See [Listen Fields](/configuration/shared/listen/) for details.
+
+### Fields
+
+#### users
+
+==Required==
+
+AnyTLS users.
+
+#### padding_scheme
+
+AnyTLS padding scheme line array.
+
+#### tls
+
+TLS configuration, see [TLS](/configuration/shared/tls/#inbound).

+ 39 - 0
docs/configuration/inbound/anytls.zh.md

@@ -0,0 +1,39 @@
+### 结构
+
+```json
+{
+  "type": "anytls",
+  "tag": "anytls-in",
+
+  ... // 监听字段
+
+  "users": [
+    {
+      "name": "sekai",
+      "password": "8JCsPssfgS8tiRwiMlhARg=="
+    }
+  ],
+  "padding_scheme": [],
+  "tls": {}
+}
+```
+
+### 监听字段
+
+参阅 [监听字段](/zh/configuration/shared/listen/)。
+
+### 字段
+
+#### users
+
+==必填==
+
+AnyTLS 用户。
+
+#### padding_scheme
+
+AnyTLS 填充方案行数组。
+
+#### tls
+
+TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。

+ 1 - 0
docs/configuration/inbound/index.md

@@ -30,6 +30,7 @@
 | `tuic`        | [TUIC](./tuic/)               | :material-close: |
 | `hysteria2`   | [Hysteria2](./hysteria2/)     | :material-close: |
 | `vless`       | [VLESS](./vless/)             | TCP              |
+| `anytls`      | [AnyTLS](./anytls/)           | TCP              |
 | `tun`         | [Tun](./tun/)                 | :material-close: |
 | `redirect`    | [Redirect](./redirect/)       | :material-close: |
 | `tproxy`      | [TProxy](./tproxy/)           | :material-close: |

+ 1 - 0
docs/configuration/inbound/index.zh.md

@@ -30,6 +30,7 @@
 | `tuic`        | [TUIC](./tuic/)               | :material-close: |
 | `hysteria2`   | [Hysteria2](./hysteria2/)     | :material-close: |
 | `vless`       | [VLESS](./vless/)             | TCP              |
+| `anytls`      | [AnyTLS](./anytls/)           | TCP              |
 | `tun`         | [Tun](./tun/)                 | :material-close: |
 | `redirect`    | [Redirect](./redirect/)       | :material-close: |
 | `tproxy`      | [TProxy](./tproxy/)           | :material-close: |

+ 55 - 0
docs/configuration/outbound/anytls.md

@@ -0,0 +1,55 @@
+### Structure
+
+```json
+{
+  "type": "anytls",
+  "tag": "anytls-out",
+
+  "server": "127.0.0.1",
+  "server_port": 1080,
+  "password": "8JCsPssfgS8tiRwiMlhARg==",
+  "idle_session_check_interval": "30s",
+  "idle_session_timeout": "30s",
+  "tls": {},
+
+  ... // Dial Fields
+}
+```
+
+### Fields
+
+#### server
+
+==Required==
+
+The server address.
+
+#### server_port
+
+==Required==
+
+The server port.
+
+#### password
+
+==Required==
+
+The AnyTLS password.
+
+#### idle_session_check_interval
+
+Interval checking for idle sessions. Default: 30s.
+
+#### idle_session_timeout
+
+In the check, close sessions that have been idle for longer than this. Default: 30s.
+
+#### tls
+
+==Required==
+
+TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
+
+### Dial Fields
+
+See [Dial Fields](/configuration/shared/dial/) for details.

+ 55 - 0
docs/configuration/outbound/anytls.zh.md

@@ -0,0 +1,55 @@
+### 结构
+
+```json
+{
+  "type": "anytls",
+  "tag": "anytls-out",
+
+  "server": "127.0.0.1",
+  "server_port": 1080,
+  "password": "8JCsPssfgS8tiRwiMlhARg==",
+  "idle_session_check_interval": "30s",
+  "idle_session_timeout": "30s",
+  "tls": {},
+
+  ... // 拨号字段
+}
+```
+
+### 字段
+
+#### server
+
+==必填==
+
+服务器地址。
+
+#### server_port
+
+==必填==
+
+服务器端口。
+
+#### password
+
+==必填==
+
+AnyTLS 密码。
+
+#### idle_session_check_interval
+
+检查空闲会话的时间间隔。默认值:30秒。
+
+#### idle_session_timeout
+
+在检查中,关闭闲置时间超过此值的会话。默认值:30秒。
+
+#### tls
+
+==必填==
+
+TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
+
+### 拨号字段
+
+参阅 [拨号字段](/zh/configuration/shared/dial/)。

+ 1 - 0
docs/configuration/outbound/index.md

@@ -30,6 +30,7 @@
 | `shadowtls`    | [ShadowTLS](./shadowtls/)       |
 | `tuic`         | [TUIC](./tuic/)                 |
 | `hysteria2`    | [Hysteria2](./hysteria2/)       |
+| `anytls`       | [AnyTLS](./anytls/)             |
 | `tor`          | [Tor](./tor/)                   |
 | `ssh`          | [SSH](./ssh/)                   |
 | `dns`          | [DNS](./dns/)                   |

+ 1 - 0
docs/configuration/outbound/index.zh.md

@@ -30,6 +30,7 @@
 | `shadowtls`    | [ShadowTLS](./shadowtls/)       |
 | `tuic`         | [TUIC](./tuic/)                 |
 | `hysteria2`    | [Hysteria2](./hysteria2/)       |
+| `anytls`       | [AnyTLS](./anytls/)             |
 | `tor`          | [Tor](./tor/)                   |
 | `ssh`          | [SSH](./ssh/)                   |
 | `dns`          | [DNS](./dns/)                   |

+ 1 - 0
go.mod

@@ -3,6 +3,7 @@ module github.com/sagernet/sing-box
 go 1.23.1
 
 require (
+	github.com/anytls/sing-anytls v0.0.2
 	github.com/caddyserver/certmagic v0.21.7
 	github.com/cloudflare/circl v1.6.0
 	github.com/cretz/bine v0.2.0

+ 4 - 0
go.sum

@@ -8,6 +8,10 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7V
 github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
 github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
 github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
+github.com/anytls/sing-anytls v0.0.1 h1:Hex6GFUcgATWMWL2E9YgH/7oPgwdokiIF09UQi5BEC0=
+github.com/anytls/sing-anytls v0.0.1/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
+github.com/anytls/sing-anytls v0.0.2 h1:25azSh0o/LMcIkhS4ZutgRTIGwh8O3wuOhsThVM9K9o=
+github.com/anytls/sing-anytls v0.0.2/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
 github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
 github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
 github.com/caddyserver/certmagic v0.21.7 h1:66KJioPFJwttL43KYSWk7ErSmE6LfaJgCQuhm8Sg6fg=

+ 3 - 0
include/registry.go

@@ -15,6 +15,7 @@ import (
 	"github.com/sagernet/sing-box/dns/transport/local"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing-box/protocol/anytls"
 	"github.com/sagernet/sing-box/protocol/block"
 	"github.com/sagernet/sing-box/protocol/direct"
 	protocolDNS "github.com/sagernet/sing-box/protocol/dns"
@@ -53,6 +54,7 @@ func InboundRegistry() *inbound.Registry {
 	naive.RegisterInbound(registry)
 	shadowtls.RegisterInbound(registry)
 	vless.RegisterInbound(registry)
+	anytls.RegisterInbound(registry)
 
 	registerQUICInbounds(registry)
 	registerStubForRemovedInbounds(registry)
@@ -80,6 +82,7 @@ func OutboundRegistry() *outbound.Registry {
 	ssh.RegisterOutbound(registry)
 	shadowtls.RegisterOutbound(registry)
 	vless.RegisterOutbound(registry)
+	anytls.RegisterOutbound(registry)
 
 	registerQUICOutbounds(registry)
 	registerWireGuardOutbound(registry)

+ 24 - 0
option/anytls.go

@@ -0,0 +1,24 @@
+package option
+
+import "github.com/sagernet/sing/common/json/badoption"
+
+type AnyTLSInboundOptions struct {
+	ListenOptions
+	InboundTLSOptionsContainer
+	Users         []AnyTLSUser               `json:"users,omitempty"`
+	PaddingScheme badoption.Listable[string] `json:"padding_scheme,omitempty"`
+}
+
+type AnyTLSUser struct {
+	Name     string `json:"name,omitempty"`
+	Password string `json:"password,omitempty"`
+}
+
+type AnyTLSOutboundOptions struct {
+	DialerOptions
+	ServerOptions
+	OutboundTLSOptionsContainer
+	Password                 string             `json:"password,omitempty"`
+	IdleSessionCheckInterval badoption.Duration `json:"idle_session_check_interval,omitempty"`
+	IdleSessionTimeout       badoption.Duration `json:"idle_session_timeout,omitempty"`
+}

+ 135 - 0
protocol/anytls/inbound.go

@@ -0,0 +1,135 @@
+package anytls
+
+import (
+	"context"
+	"net"
+	"strings"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/inbound"
+	"github.com/sagernet/sing-box/common/listener"
+	"github.com/sagernet/sing-box/common/tls"
+	"github.com/sagernet/sing-box/common/uot"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/auth"
+	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"
+
+	anytls "github.com/anytls/sing-anytls"
+	"github.com/anytls/sing-anytls/padding"
+)
+
+func RegisterInbound(registry *inbound.Registry) {
+	inbound.Register[option.AnyTLSInboundOptions](registry, C.TypeAnyTLS, NewInbound)
+}
+
+type Inbound struct {
+	inbound.Adapter
+	tlsConfig tls.ServerConfig
+	router    adapter.ConnectionRouterEx
+	logger    logger.ContextLogger
+	listener  *listener.Listener
+	service   *anytls.Service
+}
+
+func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.AnyTLSInboundOptions) (adapter.Inbound, error) {
+	inbound := &Inbound{
+		Adapter: inbound.NewAdapter(C.TypeAnyTLS, tag),
+		router:  uot.NewRouter(router, logger),
+		logger:  logger,
+	}
+
+	if options.TLS != nil && options.TLS.Enabled {
+		tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
+		if err != nil {
+			return nil, err
+		}
+		inbound.tlsConfig = tlsConfig
+	}
+
+	paddingScheme := padding.DefaultPaddingScheme
+	if len(options.PaddingScheme) > 0 {
+		paddingScheme = []byte(strings.Join(options.PaddingScheme, "\n"))
+	}
+
+	service, err := anytls.NewService(anytls.ServiceConfig{
+		Users: common.Map(options.Users, func(it option.AnyTLSUser) anytls.User {
+			return (anytls.User)(it)
+		}),
+		PaddingScheme: paddingScheme,
+		Handler:       (*inboundHandler)(inbound),
+		Logger:        logger,
+	})
+	if err != nil {
+		return nil, err
+	}
+	inbound.service = service
+	inbound.listener = listener.New(listener.Options{
+		Context:           ctx,
+		Logger:            logger,
+		Network:           []string{N.NetworkTCP},
+		Listen:            options.ListenOptions,
+		ConnectionHandler: inbound,
+	})
+	return inbound, nil
+}
+
+func (h *Inbound) Start(stage adapter.StartStage) error {
+	if stage != adapter.StartStateStart {
+		return nil
+	}
+	if h.tlsConfig != nil {
+		err := h.tlsConfig.Start()
+		if err != nil {
+			return err
+		}
+	}
+	return h.listener.Start()
+}
+
+func (h *Inbound) Close() error {
+	return common.Close(h.listener, h.tlsConfig)
+}
+
+func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
+	if h.tlsConfig != nil {
+		tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig)
+		if err != nil {
+			N.CloseOnHandshakeFailure(conn, onClose, err)
+			h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source, ": TLS handshake"))
+			return
+		}
+		conn = tlsConn
+	}
+	err := h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose)
+	if err != nil {
+		N.CloseOnHandshakeFailure(conn, onClose, err)
+		h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
+	}
+}
+
+type inboundHandler Inbound
+
+func (h *inboundHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
+	var metadata adapter.InboundContext
+	metadata.Inbound = h.Tag()
+	metadata.InboundType = h.Type()
+	//nolint:staticcheck
+	metadata.InboundDetour = h.listener.ListenOptions().Detour
+	//nolint:staticcheck
+	metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
+	metadata.Source = source
+	metadata.Destination = destination
+	if userName, _ := auth.UserFromContext[string](ctx); userName != "" {
+		metadata.User = userName
+		h.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", metadata.Destination)
+	} else {
+		h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
+	}
+	h.router.RouteConnectionEx(ctx, conn, metadata, onClose)
+}

+ 129 - 0
protocol/anytls/outbound.go

@@ -0,0 +1,129 @@
+package anytls
+
+import (
+	"context"
+	"net"
+	"os"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/adapter/outbound"
+	"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/common"
+	M "github.com/sagernet/sing/common/metadata"
+	N "github.com/sagernet/sing/common/network"
+	"github.com/sagernet/sing/common/uot"
+
+	anytls "github.com/anytls/sing-anytls"
+)
+
+func RegisterOutbound(registry *outbound.Registry) {
+	outbound.Register[option.AnyTLSOutboundOptions](registry, C.TypeAnyTLS, NewOutbound)
+}
+
+type Outbound struct {
+	outbound.Adapter
+	dialer    N.Dialer
+	server    M.Socksaddr
+	tlsConfig tls.Config
+	client    *anytls.Client
+	uotClient *uot.Client
+	logger    log.ContextLogger
+}
+
+func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.AnyTLSOutboundOptions) (adapter.Outbound, error) {
+	outbound := &Outbound{
+		Adapter: outbound.NewAdapterWithDialerOptions(C.TypeAnyTLS, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions),
+		server:  options.ServerOptions.Build(),
+		logger:  logger,
+	}
+	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
+	}
+	outbound.tlsConfig = tlsConfig
+
+	outboundDialer, err := dialer.NewWithOptions(dialer.Options{
+		Context: ctx,
+		Options: options.DialerOptions,
+	})
+	if err != nil {
+		return nil, err
+	}
+	outbound.dialer = outboundDialer
+
+	client, err := anytls.NewClient(ctx, anytls.ClientConfig{
+		Password:                 options.Password,
+		IdleSessionCheckInterval: options.IdleSessionCheckInterval.Build(),
+		IdleSessionTimeout:       options.IdleSessionTimeout.Build(),
+		DialOut:                  outbound.dialOut,
+		Logger:                   logger,
+	})
+	if err != nil {
+		return nil, err
+	}
+	outbound.client = client
+
+	outbound.uotClient = &uot.Client{
+		Dialer:  (anytlsDialer)(client.CreateProxy),
+		Version: uot.Version,
+	}
+	return outbound, nil
+}
+
+type anytlsDialer func(ctx context.Context, destination M.Socksaddr) (net.Conn, error)
+
+func (d anytlsDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+	return d(ctx, destination)
+}
+
+func (d anytlsDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+	return nil, os.ErrInvalid
+}
+
+func (h *Outbound) dialOut(ctx context.Context) (net.Conn, error) {
+	conn, err := h.dialer.DialContext(ctx, N.NetworkTCP, h.server)
+	if err != nil {
+		return nil, err
+	}
+	tlsConn, err := tls.ClientHandshake(ctx, conn, h.tlsConfig)
+	if err != nil {
+		common.Close(tlsConn, conn)
+		return nil, err
+	}
+	return tlsConn, nil
+}
+
+func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
+	ctx, metadata := adapter.ExtendContext(ctx)
+	metadata.Outbound = h.Tag()
+	metadata.Destination = destination
+	switch N.NetworkName(network) {
+	case N.NetworkTCP:
+		h.logger.InfoContext(ctx, "outbound connection to ", destination)
+		return h.client.CreateProxy(ctx, destination)
+	case N.NetworkUDP:
+		h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination)
+		return h.uotClient.DialContext(ctx, network, destination)
+	}
+	return nil, os.ErrInvalid
+}
+
+func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
+	ctx, metadata := adapter.ExtendContext(ctx)
+	metadata.Outbound = h.Tag()
+	metadata.Destination = destination
+	h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination)
+	return h.uotClient.ListenPacket(ctx, destination)
+}
+
+func (h *Outbound) Close() error {
+	return common.Close(h.client)
+}