diff --git a/common/dialer/tls.go b/common/dialer/tls.go index 782bc0c2..679e8a34 100644 --- a/common/dialer/tls.go +++ b/common/dialer/tls.go @@ -100,8 +100,8 @@ func NewTLS(dialer N.Dialer, serverAddress string, options option.OutboundTLSOpt } if len(certificate) > 0 { certPool := x509.NewCertPool() - if !certPool.AppendCertsFromPEM([]byte(options.Certificate)) { - return nil, E.New("failed to parse certificate:\n\n", options.Certificate) + if !certPool.AppendCertsFromPEM(certificate) { + return nil, E.New("failed to parse certificate:\n\n", certificate) } tlsConfig.RootCAs = certPool } diff --git a/constant/proxy.go b/constant/proxy.go index f2be807e..f2ca2c8e 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -12,6 +12,7 @@ const ( TypeMixed = "mixed" TypeShadowsocks = "shadowsocks" TypeVMess = "vmess" + TypeTrojan = "trojan" ) const ( diff --git a/go.mod b/go.mod index f879da7e..09ac718a 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/hashicorp/yamux v0.1.1 github.com/logrusorgru/aurora v2.0.3+incompatible github.com/oschwald/maxminddb-golang v1.9.0 - github.com/sagernet/sing v0.0.0-20220807085721-583e78e0b86a + github.com/sagernet/sing v0.0.0-20220808004927-21369d10810d github.com/sagernet/sing-dns v0.0.0-20220803121532-9e1ffb850d91 github.com/sagernet/sing-shadowsocks v0.0.0-20220801112336-a91eacdd01e1 github.com/sagernet/sing-tun v0.0.0-20220807091540-0fd822f913d9 diff --git a/go.sum b/go.sum index ea07b3f0..d4732b21 100644 --- a/go.sum +++ b/go.sum @@ -151,8 +151,8 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagernet/netlink v0.0.0-20220803045538-bdac49abf805 h1:hE+vtsjBCCPmxkRz9jZA+CicHgVkDT6H+Av5ZzskVxs= github.com/sagernet/netlink v0.0.0-20220803045538-bdac49abf805/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= -github.com/sagernet/sing v0.0.0-20220807085721-583e78e0b86a h1:EeaiaHqcGiGQdgRPHf8FPIKb17VADrncz1P27Jfli2w= -github.com/sagernet/sing v0.0.0-20220807085721-583e78e0b86a/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= +github.com/sagernet/sing v0.0.0-20220808004927-21369d10810d h1:vWzXLfdGyAYYbBpYFFHErtJlBXC59AieYMlUMAI6gw8= +github.com/sagernet/sing v0.0.0-20220808004927-21369d10810d/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= github.com/sagernet/sing-dns v0.0.0-20220803121532-9e1ffb850d91 h1:jxt2PYixIkK2i7nUGW3f+PzJagEZcbNyQddBWGuqNnw= github.com/sagernet/sing-dns v0.0.0-20220803121532-9e1ffb850d91/go.mod h1:T77zZdE2Cm6VqnFumrpwsq+kxYsbq+vWDhmjtdSl/oM= github.com/sagernet/sing-shadowsocks v0.0.0-20220801112336-a91eacdd01e1 h1:RYvOc69eSNMN0dwVugrDts41Nn7Ar/C/n/fvytvFcp4= diff --git a/inbound/builder.go b/inbound/builder.go index 9d086ada..a505068f 100644 --- a/inbound/builder.go +++ b/inbound/builder.go @@ -33,6 +33,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o return NewShadowsocks(ctx, router, logger, options.Tag, options.ShadowsocksOptions) case C.TypeVMess: return NewVMess(ctx, router, logger, options.Tag, options.VMessOptions) + case C.TypeTrojan: + return NewTrojan(ctx, router, logger, options.Tag, options.TrojanOptions) default: return nil, E.New("unknown inbound type: ", options.Type) } diff --git a/inbound/trojan.go b/inbound/trojan.go new file mode 100644 index 00000000..17ee7788 --- /dev/null +++ b/inbound/trojan.go @@ -0,0 +1,120 @@ +package inbound + +import ( + "context" + "crypto/tls" + "net" + "os" + + "github.com/sagernet/sing-box/adapter" + 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" + F "github.com/sagernet/sing/common/format" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/protocol/trojan" +) + +var _ adapter.Inbound = (*Trojan)(nil) + +type Trojan struct { + myInboundAdapter + service *trojan.Service[int] + users []option.TrojanUser + tlsConfig *TLSConfig +} + +func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrojanInboundOptions) (*Trojan, error) { + inbound := &Trojan{ + myInboundAdapter: myInboundAdapter{ + protocol: C.TypeTrojan, + network: []string{N.NetworkTCP}, + ctx: ctx, + router: router, + logger: logger, + tag: tag, + listenOptions: options.ListenOptions, + }, + users: options.Users, + } + service := trojan.NewService[int](adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound)) + err := service.UpdateUsers(common.MapIndexed(options.Users, func(index int, it option.TrojanUser) int { + return index + }), common.Map(options.Users, func(it option.TrojanUser) string { + return it.Password + })) + if err != nil { + return nil, err + } + if options.TLS != nil { + tlsConfig, err := NewTLSConfig(logger, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + inbound.tlsConfig = tlsConfig + } + inbound.service = service + inbound.connHandler = inbound + return inbound, nil +} + +func (h *Trojan) Start() error { + if h.tlsConfig != nil { + err := h.tlsConfig.Start() + if err != nil { + return E.Cause(err, "create TLS config") + } + } + return common.Start( + h.service, + &h.myInboundAdapter, + ) +} + +func (h *Trojan) Close() error { + return common.Close( + h.service, + &h.myInboundAdapter, + common.PtrOrNil(h.tlsConfig), + ) +} + +func (h *Trojan) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + if h.tlsConfig != nil { + conn = tls.Server(conn, h.tlsConfig.Config()) + } + 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 { + return os.ErrInvalid + } + user := h.users[userIndex].Name + if user == "" { + user = F.ToString(userIndex) + } else { + metadata.User = user + } + h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) + return h.router.RouteConnection(ctx, conn, metadata) +} + +func (h *Trojan) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + userIndex, loaded := auth.UserFromContext[int](ctx) + if !loaded { + return os.ErrInvalid + } + user := h.users[userIndex].Name + if user == "" { + user = F.ToString(userIndex) + } else { + metadata.User = user + } + h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) + return h.router.RoutePacketConnection(ctx, conn, metadata) +} diff --git a/option/inbound.go b/option/inbound.go index e5c691ec..33fe5321 100644 --- a/option/inbound.go +++ b/option/inbound.go @@ -18,6 +18,7 @@ type _Inbound struct { MixedOptions HTTPMixedInboundOptions `json:"-"` ShadowsocksOptions ShadowsocksInboundOptions `json:"-"` VMessOptions VMessInboundOptions `json:"-"` + TrojanOptions TrojanInboundOptions `json:"-"` } type Inbound _Inbound @@ -43,6 +44,8 @@ func (h Inbound) MarshalJSON() ([]byte, error) { v = h.ShadowsocksOptions case C.TypeVMess: v = h.VMessOptions + case C.TypeTrojan: + v = h.TrojanOptions default: return nil, E.New("unknown inbound type: ", h.Type) } @@ -74,6 +77,8 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error { v = &h.ShadowsocksOptions case C.TypeVMess: v = &h.VMessOptions + case C.TypeTrojan: + v = &h.TrojanOptions default: return E.New("unknown inbound type: ", h.Type) } diff --git a/option/outbound.go b/option/outbound.go index ae88e6d7..d7faf04d 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -15,6 +15,7 @@ type _Outbound struct { HTTPOptions HTTPOutboundOptions `json:"-"` ShadowsocksOptions ShadowsocksOutboundOptions `json:"-"` VMessOptions VMessOutboundOptions `json:"-"` + TrojanOptions TrojanOutboundOptions `json:"-"` SelectorOptions SelectorOutboundOptions `json:"-"` } @@ -35,6 +36,8 @@ func (h Outbound) MarshalJSON() ([]byte, error) { v = h.ShadowsocksOptions case C.TypeVMess: v = h.VMessOptions + case C.TypeTrojan: + v = h.TrojanOptions case C.TypeSelector: v = h.SelectorOptions default: @@ -62,6 +65,8 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error { v = &h.ShadowsocksOptions case C.TypeVMess: v = &h.VMessOptions + case C.TypeTrojan: + v = &h.TrojanOptions case C.TypeSelector: v = &h.SelectorOptions default: diff --git a/option/trojan.go b/option/trojan.go new file mode 100644 index 00000000..4ffe9e7d --- /dev/null +++ b/option/trojan.go @@ -0,0 +1,21 @@ +package option + +type TrojanInboundOptions struct { + ListenOptions + Users []TrojanUser `json:"users,omitempty"` + TLS *InboundTLSOptions `json:"tls,omitempty"` +} + +type TrojanUser struct { + Name string `json:"name"` + Password string `json:"password"` +} + +type TrojanOutboundOptions struct { + OutboundDialerOptions + ServerOptions + Password string `json:"password"` + Network NetworkList `json:"network,omitempty"` + TLSOptions *OutboundTLSOptions `json:"tls,omitempty"` + MultiplexOptions *MultiplexOptions `json:"multiplex,omitempty"` +} diff --git a/outbound/builder.go b/outbound/builder.go index e0632704..32f7114d 100644 --- a/outbound/builder.go +++ b/outbound/builder.go @@ -29,6 +29,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o return NewShadowsocks(ctx, router, logger, options.Tag, options.ShadowsocksOptions) case C.TypeVMess: return NewVMess(ctx, router, logger, options.Tag, options.VMessOptions) + case C.TypeTrojan: + return NewTrojan(ctx, router, logger, options.Tag, options.TrojanOptions) case C.TypeSelector: return NewSelector(router, logger, options.Tag, options.SelectorOptions) default: diff --git a/outbound/trojan.go b/outbound/trojan.go new file mode 100644 index 00000000..361221c3 --- /dev/null +++ b/outbound/trojan.go @@ -0,0 +1,122 @@ +package outbound + +import ( + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/mux" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + 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/protocol/trojan" +) + +var _ adapter.Outbound = (*Trojan)(nil) + +type Trojan struct { + myOutboundAdapter + dialer N.Dialer + serverAddr M.Socksaddr + key [56]byte + multiplexDialer N.Dialer +} + +func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrojanOutboundOptions) (*Trojan, error) { + inbound := &Trojan{ + myOutboundAdapter: myOutboundAdapter{ + protocol: C.TypeTrojan, + network: options.Network.Build(), + router: router, + logger: logger, + tag: tag, + }, + serverAddr: options.ServerOptions.Build(), + key: trojan.Key(options.Password), + } + var err error + inbound.dialer, err = dialer.NewTLS(dialer.NewOutbound(router, options.OutboundDialerOptions), options.Server, common.PtrValueOrDefault(options.TLSOptions)) + if err != nil { + return nil, err + } + inbound.multiplexDialer, err = mux.NewClientWithOptions(ctx, (*TrojanDialer)(inbound), common.PtrValueOrDefault(options.MultiplexOptions)) + if err != nil { + return nil, err + } + return inbound, nil +} + +func (h *Trojan) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + if h.multiplexDialer == nil { + switch N.NetworkName(network) { + case N.NetworkTCP: + h.logger.InfoContext(ctx, "outbound connection to ", destination) + case N.NetworkUDP: + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + } + return (*TrojanDialer)(h).DialContext(ctx, network, destination) + } else { + switch N.NetworkName(network) { + case N.NetworkTCP: + h.logger.InfoContext(ctx, "outbound multiplex connection to ", destination) + case N.NetworkUDP: + h.logger.InfoContext(ctx, "outbound multiplex packet connection to ", destination) + } + return h.multiplexDialer.DialContext(ctx, network, destination) + } +} + +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) + } else { + h.logger.InfoContext(ctx, "outbound multiplex packet connection to ", destination) + return h.multiplexDialer.ListenPacket(ctx, destination) + } +} + +func (h *Trojan) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + return NewEarlyConnection(ctx, h, conn, metadata) +} + +func (h *Trojan) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + return NewPacketConnection(ctx, h, conn, metadata) +} + +type TrojanDialer Trojan + +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 + 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 + 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 + default: + return nil, E.Extend(N.ErrUnknownNetwork, network) + } +} + +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 + } + return conn.(*trojan.ClientPacketConn), nil +} diff --git a/test/clash_test.go b/test/clash_test.go index 90ceff7a..c3a39379 100644 --- a/test/clash_test.go +++ b/test/clash_test.go @@ -29,12 +29,14 @@ const ( ImageShadowsocksRustServer = "ghcr.io/shadowsocks/ssserver-rust:latest" ImageShadowsocksRustClient = "ghcr.io/shadowsocks/sslocal-rust:latest" ImageV2RayCore = "v2fly/v2fly-core:latest" + ImageTrojan = "trojangfw/trojan:latest" ) var allImages = []string{ ImageShadowsocksRustServer, ImageShadowsocksRustClient, ImageV2RayCore, + ImageTrojan, } var localIP = netip.MustParseAddr("127.0.0.1") diff --git a/test/config/example.org-key.pem b/test/config/example.org-key.pem new file mode 100644 index 00000000..dbe9a3db --- /dev/null +++ b/test/config/example.org-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDQ+c++LkDTdaw5 +5spCu9MWMcvVdrYBZZ5qZy7DskphSUSQp25cIu34GJXVPNxtbWx1CQCmdLlwqXvo +PfUt5/pz9qsfhdAbzFduZQgGd7GTQOTJBDrAhm2+iVsQyGHHhF68muN+SgT+AtRE +sJyZoHNYtjjWEIHQ++FHEDqwUVnj6Ut99LHlyfCjOZ5+WyBiKCjyMNots/gDep7R +i4X2kMTqNMIIqPUcAaP5EQk41bJbFhKe915qN9b1dRISKFKmiWeOsxgTB/O/EaL5 +LsBYwZ/BiIMDk30aZvzRJeloasIR3z4hrKQqBfB0lfeIdiPpJIs5rXJQEiWH89ge +gplsLbfrAgMBAAECggEBAKpMGaZzDPMF/v8Ee6lcZM2+cMyZPALxa+JsCakCvyh+ +y7hSKVY+RM0cQ+YM/djTBkJtvrDniEMuasI803PAitI7nwJGSuyMXmehP6P9oKFO +jeLeZn6ETiSqzKJlmYE89vMeCevdqCnT5mW/wy5Smg0eGj0gIJpM2S3PJPSQpv9Z +ots0JXkwooJcpGWzlwPkjSouY2gDbE4Coi+jmYLNjA1k5RbggcutnUCZZkJ6yMNv +H52VjnkffpAFHRouK/YgF+5nbMyyw5YTLOyTWBq7qfBMsXynkWLU73GC/xDZa3yG +o/Ph2knXCjgLmCRessTOObdOXedjnGWIjiqF8fVboDECgYEA6x5CteYiwthDBULZ +CG5nE9VKkRHJYdArm+VjmGbzK51tKli112avmU4r3ol907+mEa4tWLkPqdZrrL49 +aHltuHizZJixJcw0rcI302ot/Ov0gkF9V55gnAQS/Kemvx9FHWm5NHdYvbObzj33 +bYRLJBtJWzYg9M8Bw9ZrUnegc/MCgYEA44kq5OSYCbyu3eaX8XHTtFhuQHNFjwl7 +Xk/Oel6PVZzmt+oOlDHnOfGSB/KpR3YXxFRngiiPZzbrOwFyPGe7HIfg03HAXiJh +ivEfrPHbQqQUI/4b44GpDy6bhNtz777ivFGYEt21vpwd89rFiye+RkqF8eL/evxO +pUayDZYvwikCgYEA07wFoZ/lkAiHmpZPsxsRcrfzFd+pto9splEWtumHdbCo3ajT +4W5VFr9iHF8/VFDT8jokFjFaXL1/bCpKTOqFl8oC68XiSkKy8gPkmFyXm5y2LhNi +GGTFZdr5alRkgttbN5i9M/WCkhvMZRhC2Xp43MRB9IUzeqNtWHqhXbvjYGcCgYEA +vTMOztviLJ6PjYa0K5lp31l0+/SeD21j/y0/VPOSHi9kjeN7EfFZAw6DTkaSShDB +fIhutYVCkSHSgfMW6XGb3gKCiW/Z9KyEDYOowicuGgDTmoYu7IOhbzVjLhtJET7Z +zJvQZ0eiW4f3RBFTF/4JMuu+6z7FD6ADSV06qx+KQNkCgYBw26iQxmT5e/4kVv8X +DzBJ1HuliKBnnzZA1YRjB4H8F6Yrq+9qur1Lurez4YlbkGV8yPFt+Iu82ViUWL28 +9T7Jgp3TOpf8qOqsWFv8HldpEZbE0Tcib4x6s+zOg/aw0ac/xOPY1sCVFB81VODP +XCar+uxMBXI1zbXqd9QdEwy4Ig== +-----END PRIVATE KEY----- diff --git a/test/config/example.org.pem b/test/config/example.org.pem new file mode 100644 index 00000000..9b99259a --- /dev/null +++ b/test/config/example.org.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIESzCCArOgAwIBAgIQIi5xRZvFZaSweWU9Y5mExjANBgkqhkiG9w0BAQsFADCB +hzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMS4wLAYDVQQLDCVkcmVh +bWFjcm9ARHJlYW1hY3JvLmxvY2FsIChEcmVhbWFjcm8pMTUwMwYDVQQDDCxta2Nl +cnQgZHJlYW1hY3JvQERyZWFtYWNyby5sb2NhbCAoRHJlYW1hY3JvKTAeFw0yMTAz +MTcxNDQwMzZaFw0yMzA2MTcxNDQwMzZaMFkxJzAlBgNVBAoTHm1rY2VydCBkZXZl +bG9wbWVudCBjZXJ0aWZpY2F0ZTEuMCwGA1UECwwlZHJlYW1hY3JvQERyZWFtYWNy +by5sb2NhbCAoRHJlYW1hY3JvKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAND5z74uQNN1rDnmykK70xYxy9V2tgFlnmpnLsOySmFJRJCnblwi7fgYldU8 +3G1tbHUJAKZ0uXCpe+g99S3n+nP2qx+F0BvMV25lCAZ3sZNA5MkEOsCGbb6JWxDI +YceEXrya435KBP4C1ESwnJmgc1i2ONYQgdD74UcQOrBRWePpS330seXJ8KM5nn5b +IGIoKPIw2i2z+AN6ntGLhfaQxOo0wgio9RwBo/kRCTjVslsWEp73Xmo31vV1EhIo +UqaJZ46zGBMH878RovkuwFjBn8GIgwOTfRpm/NEl6WhqwhHfPiGspCoF8HSV94h2 +I+kkizmtclASJYfz2B6CmWwtt+sCAwEAAaNgMF4wDgYDVR0PAQH/BAQDAgWgMBMG +A1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaAFO800LQ6Pa85RH4EbMmFH6ln +F150MBYGA1UdEQQPMA2CC2V4YW1wbGUub3JnMA0GCSqGSIb3DQEBCwUAA4IBgQAP +TsF53h7bvJcUXT3Y9yZ2vnW6xr9r92tNnM1Gfo3D2Yyn9oLf2YrfJng6WZ04Fhqa +Wh0HOvE0n6yPNpm/Q7mh64DrgolZ8Ce5H4RTJDAabHU9XhEzfGSVtzRSFsz+szu1 +Y30IV+08DxxqMmNPspYdpAET2Lwyk2WhnARGiGw11CRkQCEkVEe6d702vS9UGBUz +Du6lmCYCm0SbFrZ0CGgmHSHoTcCtf3EjVam7dPg3yWiPbWjvhXxgip6hz9sCqkhG +WA5f+fPgSZ1I9U4i+uYnqjfrzwgC08RwUYordm15F6gPvXw+KVwDO8yUYQoEH0b6 +AFJtbzoAXDysvBC6kWYFFOr62EaisaEkELTS/NrPD9ux1eKbxcxHCwEtVjgC0CL6 +gAxEAQ+9maJMbrAFhsOBbGGFC+mMCGg4eEyx6+iMB0oQe0W7QFeRUAFi7Ptc/ocS +tZ9lbrfX1/wrcTTWIYWE+xH6oeb4fhs29kxjHcf2l+tQzmpl0aP3Z/bMW4BSB+w= +-----END CERTIFICATE----- diff --git a/test/config/trojan.json b/test/config/trojan.json new file mode 100644 index 00000000..d645fc7a --- /dev/null +++ b/test/config/trojan.json @@ -0,0 +1,40 @@ +{ + "run_type": "server", + "local_addr": "0.0.0.0", + "local_port": 10000, + "password": [ + "password" + ], + "log_level": 1, + "ssl": { + "cert": "/path/to/certificate.crt", + "key": "/path/to/private.key", + "key_password": "", + "cipher": "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384", + "cipher_tls13": "TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384", + "prefer_server_cipher": true, + "alpn": [ + "http/1.1" + ], + "alpn_port_override": { + "h2": 81 + }, + "reuse_session": true, + "session_ticket": false, + "session_timeout": 600, + "plain_http_response": "", + "curves": "", + "dhparam": "" + }, + "tcp": { + "prefer_ipv4": false, + "no_delay": true, + "keep_alive": true, + "reuse_port": false, + "fast_open": false, + "fast_open_qlen": 20 + }, + "mysql": { + "enabled": false + } +} \ No newline at end of file diff --git a/test/docker_test.go b/test/docker_test.go index dc203ca5..94cc910d 100644 --- a/test/docker_test.go +++ b/test/docker_test.go @@ -3,6 +3,7 @@ package main import ( "context" "os" + "path/filepath" "testing" "time" @@ -24,7 +25,7 @@ type DockerOptions struct { Ports []uint16 Cmd []string Env []string - Bind []string + Bind map[string]string Stdin []byte } @@ -67,6 +68,15 @@ func startDockerContainer(t *testing.T, options DockerOptions) { } } + if len(options.Bind) > 0 { + hostOptions.Binds = []string{} + for path, internalPath := range options.Bind { + path = filepath.Join("config", path) + path, _ = filepath.Abs(path) + hostOptions.Binds = append(hostOptions.Binds, path+":"+internalPath) + } + } + dockerContainer, err := dockerClient.ContainerCreate(context.Background(), &containerOptions, &hostOptions, nil, nil, "") require.NoError(t, err) t.Cleanup(func() { diff --git a/test/go.mod b/test/go.mod index 7ccc4095..6769dd1b 100644 --- a/test/go.mod +++ b/test/go.mod @@ -10,7 +10,7 @@ require ( github.com/docker/docker v20.10.17+incompatible github.com/docker/go-connections v0.4.0 github.com/gofrs/uuid v4.2.0+incompatible - github.com/sagernet/sing v0.0.0-20220807085721-583e78e0b86a + github.com/sagernet/sing v0.0.0-20220808004927-21369d10810d github.com/sagernet/sing-shadowsocks v0.0.0-20220801112336-a91eacdd01e1 github.com/spyzhov/ajson v0.7.1 github.com/stretchr/testify v1.8.0 diff --git a/test/go.sum b/test/go.sum index 8886f768..ee07c47b 100644 --- a/test/go.sum +++ b/test/go.sum @@ -176,8 +176,8 @@ github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/sagernet/netlink v0.0.0-20220803045538-bdac49abf805 h1:hE+vtsjBCCPmxkRz9jZA+CicHgVkDT6H+Av5ZzskVxs= github.com/sagernet/netlink v0.0.0-20220803045538-bdac49abf805/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= -github.com/sagernet/sing v0.0.0-20220807085721-583e78e0b86a h1:EeaiaHqcGiGQdgRPHf8FPIKb17VADrncz1P27Jfli2w= -github.com/sagernet/sing v0.0.0-20220807085721-583e78e0b86a/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= +github.com/sagernet/sing v0.0.0-20220808004927-21369d10810d h1:vWzXLfdGyAYYbBpYFFHErtJlBXC59AieYMlUMAI6gw8= +github.com/sagernet/sing v0.0.0-20220808004927-21369d10810d/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY= github.com/sagernet/sing-dns v0.0.0-20220803121532-9e1ffb850d91 h1:jxt2PYixIkK2i7nUGW3f+PzJagEZcbNyQddBWGuqNnw= github.com/sagernet/sing-dns v0.0.0-20220803121532-9e1ffb850d91/go.mod h1:T77zZdE2Cm6VqnFumrpwsq+kxYsbq+vWDhmjtdSl/oM= github.com/sagernet/sing-shadowsocks v0.0.0-20220801112336-a91eacdd01e1 h1:RYvOc69eSNMN0dwVugrDts41Nn7Ar/C/n/fvytvFcp4= diff --git a/test/trojan_test.go b/test/trojan_test.go new file mode 100644 index 00000000..f185cc75 --- /dev/null +++ b/test/trojan_test.go @@ -0,0 +1,129 @@ +package main + +import ( + "net/netip" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" +) + +func TestTrojanOutbound(t *testing.T) { + startDockerContainer(t, DockerOptions{ + Image: ImageTrojan, + Ports: []uint16{serverPort, testPort}, + Bind: map[string]string{ + "trojan.json": "/config/config.json", + "example.org.pem": "/path/to/certificate.crt", + "example.org-key.pem": "/path/to/private.key", + }, + }) + startInstance(t, option.Options{ + Log: &option.LogOptions{ + Level: "error", + }, + Inbounds: []option.Inbound{ + { + Type: C.TypeMixed, + MixedOptions: option.HTTPMixedInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.ListenAddress(netip.IPv4Unspecified()), + ListenPort: clientPort, + }, + }, + }, + }, + Outbounds: []option.Outbound{ + { + Type: C.TypeTrojan, + TrojanOptions: option.TrojanOutboundOptions{ + ServerOptions: option.ServerOptions{ + Server: "127.0.0.1", + ServerPort: serverPort, + }, + Password: "password", + TLSOptions: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: "config/example.org.pem", + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +} + +func TestTrojanSelf(t *testing.T) { + startInstance(t, option.Options{ + Log: &option.LogOptions{ + Level: "error", + Output: "stderr", + }, + 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: "password", + }, + }, + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: "config/example.org.pem", + KeyPath: "config/example.org-key.pem", + }, + }, + }, + }, + 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", + TLSOptions: &option.OutboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: "config/example.org.pem", + }, + }, + }, + }, + Route: &option.RouteOptions{ + Rules: []option.Rule{ + { + DefaultOptions: option.DefaultRule{ + Inbound: []string{"mixed-in"}, + Outbound: "trojan-out", + }, + }, + }, + }, + }) + testSuit(t, clientPort, testPort) +}