diff --git a/inbound/builder.go b/inbound/builder.go index ae5b1a6e..f9e4b18c 100644 --- a/inbound/builder.go +++ b/inbound/builder.go @@ -39,6 +39,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o return NewNaive(ctx, router, logger, options.Tag, options.NaiveOptions) case C.TypeHysteria: return NewHysteria(ctx, router, logger, options.Tag, options.HysteriaOptions) + case C.TypeShadowTLS: + return NewShadowTLS(ctx, router, logger, options.Tag, options.ShadowTLSOptions) default: return nil, E.New("unknown inbound type: ", options.Type) } diff --git a/inbound/shadowsocks.go b/inbound/shadowsocks.go index f36f40cc..e1d60950 100644 --- a/inbound/shadowsocks.go +++ b/inbound/shadowsocks.go @@ -87,15 +87,3 @@ func (h *Shadowsocks) NewPacket(ctx context.Context, conn N.PacketConn, buffer * func (h *Shadowsocks) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { return os.ErrInvalid } - -func (h *Shadowsocks) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) - return h.router.RouteConnection(ctx, conn, metadata) -} - -func (h *Shadowsocks) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - ctx = log.ContextWithNewID(ctx) - h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) - h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) - return h.router.RoutePacketConnection(ctx, conn, metadata) -} diff --git a/inbound/shadowtls.go b/inbound/shadowtls.go index 63c00b58..fcc22cc6 100644 --- a/inbound/shadowtls.go +++ b/inbound/shadowtls.go @@ -1,7 +1,93 @@ package inbound +import ( + "bytes" + "context" + "encoding/binary" + "io" + "net" + + "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" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/task" +) + type ShadowTLS struct { myInboundAdapter + handshakeDialer N.Dialer + handshakeAddr M.Socksaddr } -// TODO +func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowTLSInboundOptions) (*ShadowTLS, error) { + inbound := &ShadowTLS{ + myInboundAdapter: myInboundAdapter{ + protocol: C.TypeShadowTLS, + network: []string{N.NetworkTCP}, + ctx: ctx, + router: router, + logger: logger, + tag: tag, + listenOptions: options.ListenOptions, + }, + handshakeDialer: dialer.New(router, options.Handshake.DialerOptions), + handshakeAddr: options.Handshake.ServerOptions.Build(), + } + inbound.connHandler = inbound + return inbound, nil +} + +func (s *ShadowTLS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + handshakeConn, err := s.handshakeDialer.DialContext(ctx, N.NetworkTCP, s.handshakeAddr) + if err != nil { + return err + } + var handshake task.Group + handshake.Append("client handshake", func(ctx context.Context) error { + return s.copyUntilHandshakeFinished(handshakeConn, conn) + }) + handshake.Append("server handshake", func(ctx context.Context) error { + return s.copyUntilHandshakeFinished(conn, handshakeConn) + }) + handshake.FastFail() + err = handshake.Run(ctx) + if err != nil { + return err + } + return s.newConnection(ctx, conn, metadata) +} + +func (s *ShadowTLS) copyUntilHandshakeFinished(dst io.Writer, src io.Reader) error { + const handshake = 0x16 + const changeCipherSpec = 0x14 + var hasSeenChangeCipherSpec bool + var tlsHdr [5]byte + for { + _, err := io.ReadFull(src, tlsHdr[:]) + if err != nil { + return err + } + length := binary.BigEndian.Uint16(tlsHdr[3:]) + _, err = io.Copy(dst, io.MultiReader(bytes.NewReader(tlsHdr[:]), io.LimitReader(src, int64(length)))) + if err != nil { + return err + } + if tlsHdr[0] != handshake { + if tlsHdr[0] != changeCipherSpec { + return E.New("unexpected tls frame type: ", tlsHdr[0]) + } + if !hasSeenChangeCipherSpec { + hasSeenChangeCipherSpec = true + continue + } + } + if hasSeenChangeCipherSpec { + return nil + } + } +} diff --git a/option/inbound.go b/option/inbound.go index 80092d75..550e4647 100644 --- a/option/inbound.go +++ b/option/inbound.go @@ -21,6 +21,7 @@ type _Inbound struct { TrojanOptions TrojanInboundOptions `json:"-"` NaiveOptions NaiveInboundOptions `json:"-"` HysteriaOptions HysteriaInboundOptions `json:"-"` + ShadowTLSOptions ShadowTLSInboundOptions `json:"-"` } type Inbound _Inbound @@ -52,6 +53,8 @@ func (h Inbound) MarshalJSON() ([]byte, error) { v = h.NaiveOptions case C.TypeHysteria: v = h.HysteriaOptions + case C.TypeShadowTLS: + v = h.ShadowTLSOptions default: return nil, E.New("unknown inbound type: ", h.Type) } @@ -89,6 +92,8 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error { v = &h.NaiveOptions case C.TypeHysteria: v = &h.HysteriaOptions + case C.TypeShadowTLS: + v = &h.ShadowTLSOptions default: return E.New("unknown inbound type: ", h.Type) } diff --git a/option/shadowtls.go b/option/shadowtls.go index 3d9a6e27..7db12556 100644 --- a/option/shadowtls.go +++ b/option/shadowtls.go @@ -2,8 +2,12 @@ package option type ShadowTLSInboundOptions struct { ListenOptions - Network NetworkList `json:"network,omitempty"` - HandshakeDetour string `json:"handshake_detour,omitempty"` + Handshake ShadowTLSHandshakeOptions `json:"handshake"` +} + +type ShadowTLSHandshakeOptions struct { + ServerOptions + DialerOptions } type ShadowTLSOutboundOptions struct { diff --git a/outbound/shadowtls.go b/outbound/shadowtls.go index ec9a141e..71586bc9 100644 --- a/outbound/shadowtls.go +++ b/outbound/shadowtls.go @@ -40,6 +40,8 @@ func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.Context if options.TLS == nil || !options.TLS.Enabled { return nil, C.ErrTLSRequired } + options.TLS.MinVersion = "1.2" + options.TLS.MaxVersion = "1.2" var err error outbound.tlsConfig, err = dialer.TLSConfig(options.Server, common.PtrValueOrDefault(options.TLS)) if err != nil { diff --git a/route/router.go b/route/router.go index 42c3fadf..d09065b4 100644 --- a/route/router.go +++ b/route/router.go @@ -519,8 +519,9 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad if !common.Contains(injectable.Network(), N.NetworkTCP) { return E.New("inject: TCP unsupported") } - metadata.InboundDetour = "" metadata.LastInbound = metadata.Inbound + metadata.Inbound = metadata.InboundDetour + metadata.InboundDetour = "" err := injectable.NewConnection(ctx, conn, metadata) if err != nil { return E.Cause(err, "inject ", detour.Tag()) @@ -599,8 +600,9 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m if !common.Contains(injectable.Network(), N.NetworkUDP) { return E.New("inject: UDP unsupported") } - metadata.InboundDetour = "" metadata.LastInbound = metadata.Inbound + metadata.Inbound = metadata.InboundDetour + metadata.InboundDetour = "" err := injectable.NewPacketConnection(ctx, conn, metadata) if err != nil { return E.Cause(err, "inject ", detour.Tag()) diff --git a/test/shadowtls_test.go b/test/shadowtls_test.go index 32dbd560..2e9c088c 100644 --- a/test/shadowtls_test.go +++ b/test/shadowtls_test.go @@ -6,9 +6,99 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-shadowsocks/shadowaead_2022" F "github.com/sagernet/sing/common/format" ) +func TestShadowTLS(t *testing.T) { + method := shadowaead_2022.List[0] + password := mkBase64(t, 16) + 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.TypeShadowTLS, + Tag: "in", + ShadowTLSOptions: option.ShadowTLSInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.ListenAddress(netip.IPv4Unspecified()), + ListenPort: serverPort, + Detour: "detour", + }, + Handshake: option.ShadowTLSHandshakeOptions{ + ServerOptions: option.ServerOptions{ + Server: "google.com", + ServerPort: 443, + }, + }, + }, + }, + { + Type: C.TypeShadowsocks, + Tag: "detour", + ShadowsocksOptions: option.ShadowsocksInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.ListenAddress(netip.IPv4Unspecified()), + ListenPort: otherPort, + }, + Method: method, + Password: password, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeShadowsocks, + ShadowsocksOptions: option.ShadowsocksOutboundOptions{ + Method: method, + Password: password, + 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) +} + func TestShadowTLSOutbound(t *testing.T) { startDockerContainer(t, DockerOptions{ Image: ImageShadowTLS,