diff --git a/constant/proxy.go b/constant/proxy.go index 45cb1484..8ea820e9 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -18,6 +18,7 @@ const ( TypeHysteria = "hysteria" TypeTor = "tor" TypeSSH = "ssh" + TypeShadowTLS = "shadowtls" ) const ( diff --git a/inbound/shadowtls.go b/inbound/shadowtls.go new file mode 100644 index 00000000..63c00b58 --- /dev/null +++ b/inbound/shadowtls.go @@ -0,0 +1,7 @@ +package inbound + +type ShadowTLS struct { + myInboundAdapter +} + +// TODO diff --git a/option/outbound.go b/option/outbound.go index aa53a3a3..b4ca57aa 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -20,6 +20,7 @@ type _Outbound struct { HysteriaOptions HysteriaOutboundOptions `json:"-"` TorOptions TorOutboundOptions `json:"-"` SSHOptions SSHOutboundOptions `json:"-"` + ShadowTLSOptions ShadowTLSOutboundOptions `json:"-"` SelectorOptions SelectorOutboundOptions `json:"-"` } @@ -50,6 +51,8 @@ func (h Outbound) MarshalJSON() ([]byte, error) { v = h.TorOptions case C.TypeSSH: v = h.SSHOptions + case C.TypeShadowTLS: + v = h.ShadowTLSOptions case C.TypeSelector: v = h.SelectorOptions default: @@ -87,6 +90,8 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error { v = &h.TorOptions case C.TypeSSH: v = &h.SSHOptions + case C.TypeShadowTLS: + v = &h.ShadowTLSOptions case C.TypeSelector: v = &h.SelectorOptions default: diff --git a/option/shadowtls.go b/option/shadowtls.go new file mode 100644 index 00000000..3d9a6e27 --- /dev/null +++ b/option/shadowtls.go @@ -0,0 +1,13 @@ +package option + +type ShadowTLSInboundOptions struct { + ListenOptions + Network NetworkList `json:"network,omitempty"` + HandshakeDetour string `json:"handshake_detour,omitempty"` +} + +type ShadowTLSOutboundOptions struct { + OutboundDialerOptions + ServerOptions + TLS *OutboundTLSOptions `json:"tls,omitempty"` +} diff --git a/outbound/builder.go b/outbound/builder.go index fff9eb42..fc00044f 100644 --- a/outbound/builder.go +++ b/outbound/builder.go @@ -39,6 +39,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o return NewTor(ctx, router, logger, options.Tag, options.TorOptions) case C.TypeSSH: return NewSSH(ctx, router, logger, options.Tag, options.SSHOptions) + case C.TypeShadowTLS: + return NewShadowTLS(ctx, router, logger, options.Tag, options.ShadowTLSOptions) case C.TypeSelector: return NewSelector(router, logger, options.Tag, options.SelectorOptions) default: diff --git a/outbound/shadowtls.go b/outbound/shadowtls.go new file mode 100644 index 00000000..ec9a141e --- /dev/null +++ b/outbound/shadowtls.go @@ -0,0 +1,82 @@ +package outbound + +import ( + "context" + "crypto/tls" + "net" + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + 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" +) + +var _ adapter.Outbound = (*ShadowTLS)(nil) + +type ShadowTLS struct { + myOutboundAdapter + dialer N.Dialer + serverAddr M.Socksaddr + tlsConfig *tls.Config +} + +func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowTLSOutboundOptions) (*ShadowTLS, error) { + outbound := &ShadowTLS{ + myOutboundAdapter: myOutboundAdapter{ + protocol: C.TypeShadowTLS, + network: []string{N.NetworkTCP}, + router: router, + logger: logger, + tag: tag, + }, + dialer: dialer.NewOutbound(router, options.OutboundDialerOptions), + serverAddr: options.ServerOptions.Build(), + } + if options.TLS == nil || !options.TLS.Enabled { + return nil, C.ErrTLSRequired + } + var err error + outbound.tlsConfig, err = dialer.TLSConfig(options.Server, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + return outbound, nil +} + +func (s *ShadowTLS) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + switch N.NetworkName(network) { + case N.NetworkTCP: + default: + return nil, os.ErrInvalid + } + conn, err := s.dialer.DialContext(ctx, N.NetworkTCP, s.serverAddr) + if err != nil { + return nil, err + } + tlsConn, err := dialer.TLSClient(ctx, conn, s.tlsConfig) + if err != nil { + return nil, err + } + err = tlsConn.HandshakeContext(ctx) + if err != nil { + return nil, err + } + return conn, nil +} + +func (s *ShadowTLS) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + return nil, os.ErrInvalid +} + +func (s *ShadowTLS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + return NewConnection(ctx, s, conn, metadata) +} + +func (s *ShadowTLS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + return os.ErrInvalid +} diff --git a/test/box_test.go b/test/box_test.go index 1d824ee4..ee8247fd 100644 --- a/test/box_test.go +++ b/test/box_test.go @@ -9,6 +9,7 @@ import ( "github.com/sagernet/sing-box" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common/bufio" + "github.com/sagernet/sing/common/debug" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/protocol/socks" @@ -17,6 +18,11 @@ import ( ) func startInstance(t *testing.T, options option.Options) { + if debug.Enabled { + options.Log = &option.LogOptions{ + Level: "trace", + } + } var instance *box.Box var err error for retry := 0; retry < 3; retry++ { diff --git a/test/clash_test.go b/test/clash_test.go index da3f7314..076fb115 100644 --- a/test/clash_test.go +++ b/test/clash_test.go @@ -36,6 +36,7 @@ const ( ImageBoringTun = "ghcr.io/ntkme/boringtun:edge" ImageHysteria = "tobyxdd/hysteria:latest" ImageNginx = "nginx:stable" + ImageShadowTLS = "ghcr.io/ihciah/shadow-tls:latest" ) var allImages = []string{ @@ -46,7 +47,8 @@ var allImages = []string{ ImageNaive, ImageBoringTun, ImageHysteria, - // ImageNginx, + ImageNginx, + ImageShadowTLS, } var localIP = netip.MustParseAddr("127.0.0.1") diff --git a/test/shadowtls_test.go b/test/shadowtls_test.go new file mode 100644 index 00000000..32dbd560 --- /dev/null +++ b/test/shadowtls_test.go @@ -0,0 +1,82 @@ +package main + +import ( + "net/netip" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + F "github.com/sagernet/sing/common/format" +) + +func TestShadowTLSOutbound(t *testing.T) { + startDockerContainer(t, DockerOptions{ + Image: ImageShadowTLS, + Ports: []uint16{serverPort, otherPort}, + EntryPoint: "shadow-tls", + Cmd: []string{"--threads", "1", "server", "0.0.0.0:" + F.ToString(serverPort), "127.0.0.1:" + F.ToString(otherPort), "google.com:443"}, + }) + startInstance(t, option.Options{ + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + MixedOptions: option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.ListenAddress(netip.IPv4Unspecified()), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeMixed, + Tag: "detour", + MixedOptions: option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.ListenAddress(netip.IPv4Unspecified()), + ListenPort: otherPort, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeSocks, + SocksOptions: option.SocksOutboundOptions{ + OutboundDialerOptions: option.OutboundDialerOptions{ + DialerOptions: option.DialerOptions{ + Detour: "detour", + }, + }, + }, + }, + { + Type: C.TypeShadowTLS, + Tag: "detour", + ShadowTLSOptions: option.ShadowTLSOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "google.com", + MaxVersion: "1.2", + }, + }, + }, + { + Type: C.TypeDirect, + Tag: "direct", + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{{ + DefaultOptions: option.DefaultRule{ + Inbound: []string{"detour"}, + Outbound: "direct", + }, + }}, + }, + }) + testTCP(t, clientPort, testPort) +}