From c9b7acd22c16c863b36c05d58f435c04aaab84fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 23 Aug 2022 12:11:56 +0800 Subject: [PATCH] Add v2ray transport to trojan --- cmd/internal/protogen/main.go | 35 ------ inbound/trojan.go | 51 +++++++- option/trojan.go | 16 +-- outbound/trojan.go | 56 ++++++--- test/trojan_test.go | 4 +- ...nsport_test.go => v2ray_transport_test.go} | 116 ++++++++++++++++-- 6 files changed, 197 insertions(+), 81 deletions(-) rename test/{vmess_transport_test.go => v2ray_transport_test.go} (68%) diff --git a/cmd/internal/protogen/main.go b/cmd/internal/protogen/main.go index e46e7208..4d5023f7 100644 --- a/cmd/internal/protogen/main.go +++ b/cmd/internal/protogen/main.go @@ -9,9 +9,7 @@ import ( "os" "os/exec" "path/filepath" - "regexp" "runtime" - "strconv" "strings" ) @@ -81,39 +79,6 @@ func GetGOBIN() string { return GOBIN } -func getInstalledProtocVersion(protocPath string) (string, error) { - cmd := exec.Command(protocPath, "--version") - cmd.Env = append(cmd.Env, os.Environ()...) - output, cmdErr := cmd.CombinedOutput() - if cmdErr != nil { - return "", cmdErr - } - versionRegexp := regexp.MustCompile(`protoc\s*(\d+\.\d+\.\d+)`) - matched := versionRegexp.FindStringSubmatch(string(output)) - return matched[1], nil -} - -func parseVersion(s string, width int) int64 { - strList := strings.Split(s, ".") - format := fmt.Sprintf("%%s%%0%ds", width) - v := "" - for _, value := range strList { - v = fmt.Sprintf(format, v, value) - } - var result int64 - var err error - if result, err = strconv.ParseInt(v, 10, 64); err != nil { - return 0 - } - return result -} - -func needToUpdate(targetedVersion, installedVersion string) bool { - vt := parseVersion(targetedVersion, 4) - vi := parseVersion(installedVersion, 4) - return vt > vi -} - func main() { pwd, err := os.Getwd() if err != nil { diff --git a/inbound/trojan.go b/inbound/trojan.go index ff8bdfc4..60b2abaf 100644 --- a/inbound/trojan.go +++ b/inbound/trojan.go @@ -10,6 +10,7 @@ import ( 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/v2ray" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" E "github.com/sagernet/sing/common/exceptions" @@ -27,6 +28,7 @@ type Trojan struct { users []option.TrojanUser tlsConfig *TLSConfig fallbackAddr M.Socksaddr + transport adapter.V2RayServerTransport } func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrojanInboundOptions) (*Trojan, error) { @@ -63,6 +65,16 @@ func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLog } inbound.tlsConfig = tlsConfig } + if options.Transport != nil { + var tlsConfig *tls.Config + if inbound.tlsConfig != nil { + tlsConfig = inbound.tlsConfig.Config() + } + inbound.transport, err = v2ray.NewServerTransport(ctx, common.PtrValueOrDefault(options.Transport), tlsConfig, adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newTransportConnection, nil, nil), inbound) + if err != nil { + return nil, E.Cause(err, "create server transport: ", options.Transport.Type) + } + } inbound.service = service inbound.connHandler = inbound return inbound, nil @@ -75,17 +87,41 @@ func (h *Trojan) Start() error { return E.Cause(err, "create TLS config") } } - return common.Start( - h.service, - &h.myInboundAdapter, - ) + if h.transport == nil { + return h.myInboundAdapter.Start() + } + if common.Contains(h.transport.Network(), N.NetworkTCP) { + tcpListener, err := h.myInboundAdapter.ListenTCP() + if err != nil { + return err + } + go func() { + sErr := h.transport.Serve(tcpListener) + if sErr != nil && !E.IsClosed(sErr) { + h.logger.Error("transport serve error: ", sErr) + } + }() + } + if common.Contains(h.transport.Network(), N.NetworkUDP) { + udpConn, err := h.myInboundAdapter.ListenUDP() + if err != nil { + return err + } + go func() { + sErr := h.transport.ServePacket(udpConn) + if sErr != nil && !E.IsClosed(sErr) { + h.logger.Error("transport serve error: ", sErr) + } + }() + } + return nil } func (h *Trojan) Close() error { return common.Close( - h.service, &h.myInboundAdapter, common.PtrOrNil(h.tlsConfig), + h.transport, ) } @@ -96,6 +132,11 @@ func (h *Trojan) NewConnection(ctx context.Context, conn net.Conn, metadata adap return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata)) } +func (h *Trojan) newTransportConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + metadata = h.createMetadata(conn) + return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata)) +} + func (h *Trojan) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { diff --git a/option/trojan.go b/option/trojan.go index 3c4c2b13..2deb4580 100644 --- a/option/trojan.go +++ b/option/trojan.go @@ -2,9 +2,10 @@ package option type TrojanInboundOptions struct { ListenOptions - Users []TrojanUser `json:"users,omitempty"` - TLS *InboundTLSOptions `json:"tls,omitempty"` - Fallback *ServerOptions `json:"fallback,omitempty"` + Users []TrojanUser `json:"users,omitempty"` + TLS *InboundTLSOptions `json:"tls,omitempty"` + Fallback *ServerOptions `json:"fallback,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` } type TrojanUser struct { @@ -15,8 +16,9 @@ type TrojanUser struct { type TrojanOutboundOptions struct { OutboundDialerOptions ServerOptions - Password string `json:"password"` - Network NetworkList `json:"network,omitempty"` - TLSOptions *OutboundTLSOptions `json:"tls,omitempty"` - MultiplexOptions *MultiplexOptions `json:"multiplex,omitempty"` + Password string `json:"password"` + Network NetworkList `json:"network,omitempty"` + TLS *OutboundTLSOptions `json:"tls,omitempty"` + Multiplex *MultiplexOptions `json:"multiplex,omitempty"` + Transport *V2RayTransportOptions `json:"transport,omitempty"` } diff --git a/outbound/trojan.go b/outbound/trojan.go index 5d1e34c6..54f7e784 100644 --- a/outbound/trojan.go +++ b/outbound/trojan.go @@ -2,6 +2,7 @@ package outbound import ( "context" + "crypto/tls" "net" "github.com/sagernet/sing-box/adapter" @@ -10,6 +11,7 @@ import ( 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/v2ray" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" @@ -25,6 +27,8 @@ type Trojan struct { serverAddr M.Socksaddr key [56]byte multiplexDialer N.Dialer + tlsConfig *tls.Config + transport adapter.V2RayClientTransport } func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrojanOutboundOptions) (*Trojan, error) { @@ -36,15 +40,24 @@ func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLog logger: logger, tag: tag, }, + dialer: dialer.NewOutbound(router, options.OutboundDialerOptions), serverAddr: options.ServerOptions.Build(), key: trojan.Key(options.Password), } var err error - outbound.dialer, err = dialer.NewTLS(dialer.NewOutbound(router, options.OutboundDialerOptions), options.Server, common.PtrValueOrDefault(options.TLSOptions)) - if err != nil { - return nil, err + if options.TLS != nil { + outbound.tlsConfig, err = dialer.TLSConfig(options.Server, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } } - outbound.multiplexDialer, err = mux.NewClientWithOptions(ctx, (*TrojanDialer)(outbound), common.PtrValueOrDefault(options.MultiplexOptions)) + if options.Transport != nil { + outbound.transport, err = v2ray.NewClientTransport(ctx, outbound.dialer, outbound.serverAddr, common.PtrValueOrDefault(options.Transport), outbound.tlsConfig) + if err != nil { + return nil, E.Cause(err, "create client transport: ", options.Transport.Type) + } + } + outbound.multiplexDialer, err = mux.NewClientWithOptions(ctx, (*trojanDialer)(outbound), common.PtrValueOrDefault(options.Multiplex)) if err != nil { return nil, err } @@ -59,7 +72,7 @@ func (h *Trojan) DialContext(ctx context.Context, network string, destination M. case N.NetworkUDP: h.logger.InfoContext(ctx, "outbound packet connection to ", destination) } - return (*TrojanDialer)(h).DialContext(ctx, network, destination) + return (*trojanDialer)(h).DialContext(ctx, network, destination) } else { switch N.NetworkName(network) { case N.NetworkTCP: @@ -74,7 +87,7 @@ func (h *Trojan) DialContext(ctx context.Context, network string, destination M. func (h *Trojan) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { if h.multiplexDialer == nil { h.logger.InfoContext(ctx, "outbound packet connection to ", destination) - return (*TrojanDialer)(h).ListenPacket(ctx, destination) + return (*trojanDialer)(h).ListenPacket(ctx, destination) } else { h.logger.InfoContext(ctx, "outbound multiplex packet connection to ", destination) return h.multiplexDialer.ListenPacket(ctx, destination) @@ -89,31 +102,36 @@ func (h *Trojan) NewPacketConnection(ctx context.Context, conn N.PacketConn, met return NewPacketConnection(ctx, h, conn, metadata) } -type TrojanDialer Trojan +type trojanDialer Trojan -func (h *TrojanDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { +func (h *trojanDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { ctx, metadata := adapter.AppendContext(ctx) metadata.Outbound = h.tag metadata.Destination = destination + var conn net.Conn + var err error + if h.transport != nil { + conn, err = h.transport.DialContext(ctx) + } else { + conn, err = h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr) + if err == nil && h.tlsConfig != nil { + conn, err = dialer.TLSClient(ctx, conn, h.tlsConfig) + } + } + if err != nil { + return nil, err + } switch N.NetworkName(network) { case N.NetworkTCP: - outConn, err := h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr) - if err != nil { - return nil, err - } - return trojan.NewClientConn(outConn, h.key, destination), nil + return trojan.NewClientConn(conn, h.key, destination), nil case N.NetworkUDP: - outConn, err := h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr) - if err != nil { - return nil, err - } - return trojan.NewClientPacketConn(outConn, h.key), nil + return trojan.NewClientPacketConn(conn, h.key), nil default: return nil, E.Extend(N.ErrUnknownNetwork, network) } } -func (h *TrojanDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { +func (h *trojanDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { conn, err := h.DialContext(ctx, N.NetworkUDP, destination) if err != nil { return nil, err diff --git a/test/trojan_test.go b/test/trojan_test.go index 6445be67..9fe833c3 100644 --- a/test/trojan_test.go +++ b/test/trojan_test.go @@ -43,7 +43,7 @@ func TestTrojanOutbound(t *testing.T) { ServerPort: serverPort, }, Password: "password", - TLSOptions: &option.OutboundTLSOptions{ + TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, @@ -107,7 +107,7 @@ func TestTrojanSelf(t *testing.T) { ServerPort: serverPort, }, Password: "password", - TLSOptions: &option.OutboundTLSOptions{ + TLS: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", CertificatePath: certPem, diff --git a/test/vmess_transport_test.go b/test/v2ray_transport_test.go similarity index 68% rename from test/vmess_transport_test.go rename to test/v2ray_transport_test.go index 62b7ba59..0577a32e 100644 --- a/test/vmess_transport_test.go +++ b/test/v2ray_transport_test.go @@ -11,8 +11,8 @@ import ( "github.com/stretchr/testify/require" ) -func TestVMessGRPCSelf(t *testing.T) { - testVMessWebscoketSelf(t, &option.V2RayTransportOptions{ +func TestV2RayGRPCSelf(t *testing.T) { + testV2RayTransportSelf(t, &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeGRPC, GRPCOptions: option.V2RayGRPCOptions{ ServiceName: "TunService", @@ -20,14 +20,14 @@ func TestVMessGRPCSelf(t *testing.T) { }) } -func TestVMessWebscoketSelf(t *testing.T) { +func TestV2RayWebscoketSelf(t *testing.T) { t.Run("basic", func(t *testing.T) { - testVMessWebscoketSelf(t, &option.V2RayTransportOptions{ + testV2RayTransportSelf(t, &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeWebsocket, }) }) t.Run("v2ray early data", func(t *testing.T) { - testVMessWebscoketSelf(t, &option.V2RayTransportOptions{ + testV2RayTransportSelf(t, &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeWebsocket, WebsocketOptions: option.V2RayWebsocketOptions{ MaxEarlyData: 2048, @@ -35,7 +35,7 @@ func TestVMessWebscoketSelf(t *testing.T) { }) }) t.Run("xray early data", func(t *testing.T) { - testVMessWebscoketSelf(t, &option.V2RayTransportOptions{ + testV2RayTransportSelf(t, &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeWebsocket, WebsocketOptions: option.V2RayWebsocketOptions{ MaxEarlyData: 2048, @@ -45,13 +45,28 @@ func TestVMessWebscoketSelf(t *testing.T) { }) } -func TestVMessHTTPSelf(t *testing.T) { - testVMessWebscoketSelf(t, &option.V2RayTransportOptions{ +func TestV2RayHTTPSelf(t *testing.T) { + testV2RayTransportSelf(t, &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeHTTP, }) } -func testVMessWebscoketSelf(t *testing.T, transport *option.V2RayTransportOptions) { +func TestV2RayHTTPPlainSelf(t *testing.T) { + testV2RayTransportNOTLSSelf(t, &option.V2RayTransportOptions{ + Type: C.V2RayTransportTypeHTTP, + }) +} + +func testV2RayTransportSelf(t *testing.T, transport *option.V2RayTransportOptions) { + t.Run("vmess", func(t *testing.T) { + testVMessTransportSelf(t, transport) + }) + t.Run("trojan", func(t *testing.T) { + testTrojanTransportSelf(t, transport) + }) +} + +func testVMessTransportSelf(t *testing.T, transport *option.V2RayTransportOptions) { user, err := uuid.DefaultGenerator.NewV4() require.NoError(t, err) _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") @@ -130,6 +145,84 @@ func testVMessWebscoketSelf(t *testing.T, transport *option.V2RayTransportOption testSuit(t, clientPort, testPort) } +func testTrojanTransportSelf(t *testing.T, transport *option.V2RayTransportOptions) { + user, err := uuid.DefaultGenerator.NewV4() + require.NoError(t, err) + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + startInstance(t, option.Options{ + Log: &option.LogOptions{ + Level: "error", + }, + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + Tag: "mixed-in", + MixedOptions: option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.ListenAddress(netip.IPv4Unspecified()), + ListenPort: clientPort, + }, + }, + }, + { + Type: C.TypeTrojan, + TrojanOptions: option.TrojanInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.ListenAddress(netip.IPv4Unspecified()), + ListenPort: serverPort, + }, + Users: []option.TrojanUser{ + { + Name: "sekai", + Password: user.String(), + }, + }, + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + Transport: transport, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeDirect, + }, + { + Type: C.TypeTrojan, + Tag: "vmess-out", + TrojanOptions: option.TrojanOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Password: user.String(), + TLS: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + }, + Transport: transport, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + DefaultOptions: option.DefaultRule{ + Inbound: []string{"mixed-in"}, + Outbound: "vmess-out", + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + func TestVMessQUICSelf(t *testing.T) { transport := &option.V2RayTransportOptions{ Type: C.V2RayTransportTypeQUIC, @@ -212,10 +305,7 @@ func TestVMessQUICSelf(t *testing.T) { testSuitQUIC(t, clientPort, testPort) } -func TestVMessHTTPNoTLSSelf(t *testing.T) { - transport := &option.V2RayTransportOptions{ - Type: C.V2RayTransportTypeHTTP, - } +func testV2RayTransportNOTLSSelf(t *testing.T, transport *option.V2RayTransportOptions) { user, err := uuid.DefaultGenerator.NewV4() require.NoError(t, err) startInstance(t, option.Options{