浏览代码

Add trojan inbound/outbound

世界 3 年之前
父节点
当前提交
6d78cf6b58

+ 2 - 2
common/dialer/tls.go

@@ -100,8 +100,8 @@ func NewTLS(dialer N.Dialer, serverAddress string, options option.OutboundTLSOpt
 	}
 	}
 	if len(certificate) > 0 {
 	if len(certificate) > 0 {
 		certPool := x509.NewCertPool()
 		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
 		tlsConfig.RootCAs = certPool
 	}
 	}

+ 1 - 0
constant/proxy.go

@@ -12,6 +12,7 @@ const (
 	TypeMixed       = "mixed"
 	TypeMixed       = "mixed"
 	TypeShadowsocks = "shadowsocks"
 	TypeShadowsocks = "shadowsocks"
 	TypeVMess       = "vmess"
 	TypeVMess       = "vmess"
+	TypeTrojan      = "trojan"
 )
 )
 
 
 const (
 const (

+ 1 - 1
go.mod

@@ -13,7 +13,7 @@ require (
 	github.com/hashicorp/yamux v0.1.1
 	github.com/hashicorp/yamux v0.1.1
 	github.com/logrusorgru/aurora v2.0.3+incompatible
 	github.com/logrusorgru/aurora v2.0.3+incompatible
 	github.com/oschwald/maxminddb-golang v1.9.0
 	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-dns v0.0.0-20220803121532-9e1ffb850d91
 	github.com/sagernet/sing-shadowsocks v0.0.0-20220801112336-a91eacdd01e1
 	github.com/sagernet/sing-shadowsocks v0.0.0-20220801112336-a91eacdd01e1
 	github.com/sagernet/sing-tun v0.0.0-20220807091540-0fd822f913d9
 	github.com/sagernet/sing-tun v0.0.0-20220807091540-0fd822f913d9

+ 2 - 2
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/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 h1:hE+vtsjBCCPmxkRz9jZA+CicHgVkDT6H+Av5ZzskVxs=
 github.com/sagernet/netlink v0.0.0-20220803045538-bdac49abf805/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
 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 h1:jxt2PYixIkK2i7nUGW3f+PzJagEZcbNyQddBWGuqNnw=
 github.com/sagernet/sing-dns v0.0.0-20220803121532-9e1ffb850d91/go.mod h1:T77zZdE2Cm6VqnFumrpwsq+kxYsbq+vWDhmjtdSl/oM=
 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=
 github.com/sagernet/sing-shadowsocks v0.0.0-20220801112336-a91eacdd01e1 h1:RYvOc69eSNMN0dwVugrDts41Nn7Ar/C/n/fvytvFcp4=

+ 2 - 0
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)
 		return NewShadowsocks(ctx, router, logger, options.Tag, options.ShadowsocksOptions)
 	case C.TypeVMess:
 	case C.TypeVMess:
 		return NewVMess(ctx, router, logger, options.Tag, options.VMessOptions)
 		return NewVMess(ctx, router, logger, options.Tag, options.VMessOptions)
+	case C.TypeTrojan:
+		return NewTrojan(ctx, router, logger, options.Tag, options.TrojanOptions)
 	default:
 	default:
 		return nil, E.New("unknown inbound type: ", options.Type)
 		return nil, E.New("unknown inbound type: ", options.Type)
 	}
 	}

+ 120 - 0
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)
+}

+ 5 - 0
option/inbound.go

@@ -18,6 +18,7 @@ type _Inbound struct {
 	MixedOptions       HTTPMixedInboundOptions   `json:"-"`
 	MixedOptions       HTTPMixedInboundOptions   `json:"-"`
 	ShadowsocksOptions ShadowsocksInboundOptions `json:"-"`
 	ShadowsocksOptions ShadowsocksInboundOptions `json:"-"`
 	VMessOptions       VMessInboundOptions       `json:"-"`
 	VMessOptions       VMessInboundOptions       `json:"-"`
+	TrojanOptions      TrojanInboundOptions      `json:"-"`
 }
 }
 
 
 type Inbound _Inbound
 type Inbound _Inbound
@@ -43,6 +44,8 @@ func (h Inbound) MarshalJSON() ([]byte, error) {
 		v = h.ShadowsocksOptions
 		v = h.ShadowsocksOptions
 	case C.TypeVMess:
 	case C.TypeVMess:
 		v = h.VMessOptions
 		v = h.VMessOptions
+	case C.TypeTrojan:
+		v = h.TrojanOptions
 	default:
 	default:
 		return nil, E.New("unknown inbound type: ", h.Type)
 		return nil, E.New("unknown inbound type: ", h.Type)
 	}
 	}
@@ -74,6 +77,8 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error {
 		v = &h.ShadowsocksOptions
 		v = &h.ShadowsocksOptions
 	case C.TypeVMess:
 	case C.TypeVMess:
 		v = &h.VMessOptions
 		v = &h.VMessOptions
+	case C.TypeTrojan:
+		v = &h.TrojanOptions
 	default:
 	default:
 		return E.New("unknown inbound type: ", h.Type)
 		return E.New("unknown inbound type: ", h.Type)
 	}
 	}

+ 5 - 0
option/outbound.go

@@ -15,6 +15,7 @@ type _Outbound struct {
 	HTTPOptions        HTTPOutboundOptions        `json:"-"`
 	HTTPOptions        HTTPOutboundOptions        `json:"-"`
 	ShadowsocksOptions ShadowsocksOutboundOptions `json:"-"`
 	ShadowsocksOptions ShadowsocksOutboundOptions `json:"-"`
 	VMessOptions       VMessOutboundOptions       `json:"-"`
 	VMessOptions       VMessOutboundOptions       `json:"-"`
+	TrojanOptions      TrojanOutboundOptions      `json:"-"`
 	SelectorOptions    SelectorOutboundOptions    `json:"-"`
 	SelectorOptions    SelectorOutboundOptions    `json:"-"`
 }
 }
 
 
@@ -35,6 +36,8 @@ func (h Outbound) MarshalJSON() ([]byte, error) {
 		v = h.ShadowsocksOptions
 		v = h.ShadowsocksOptions
 	case C.TypeVMess:
 	case C.TypeVMess:
 		v = h.VMessOptions
 		v = h.VMessOptions
+	case C.TypeTrojan:
+		v = h.TrojanOptions
 	case C.TypeSelector:
 	case C.TypeSelector:
 		v = h.SelectorOptions
 		v = h.SelectorOptions
 	default:
 	default:
@@ -62,6 +65,8 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error {
 		v = &h.ShadowsocksOptions
 		v = &h.ShadowsocksOptions
 	case C.TypeVMess:
 	case C.TypeVMess:
 		v = &h.VMessOptions
 		v = &h.VMessOptions
+	case C.TypeTrojan:
+		v = &h.TrojanOptions
 	case C.TypeSelector:
 	case C.TypeSelector:
 		v = &h.SelectorOptions
 		v = &h.SelectorOptions
 	default:
 	default:

+ 21 - 0
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"`
+}

+ 2 - 0
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)
 		return NewShadowsocks(ctx, router, logger, options.Tag, options.ShadowsocksOptions)
 	case C.TypeVMess:
 	case C.TypeVMess:
 		return NewVMess(ctx, router, logger, options.Tag, options.VMessOptions)
 		return NewVMess(ctx, router, logger, options.Tag, options.VMessOptions)
+	case C.TypeTrojan:
+		return NewTrojan(ctx, router, logger, options.Tag, options.TrojanOptions)
 	case C.TypeSelector:
 	case C.TypeSelector:
 		return NewSelector(router, logger, options.Tag, options.SelectorOptions)
 		return NewSelector(router, logger, options.Tag, options.SelectorOptions)
 	default:
 	default:

+ 122 - 0
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
+}

+ 2 - 0
test/clash_test.go

@@ -29,12 +29,14 @@ const (
 	ImageShadowsocksRustServer = "ghcr.io/shadowsocks/ssserver-rust:latest"
 	ImageShadowsocksRustServer = "ghcr.io/shadowsocks/ssserver-rust:latest"
 	ImageShadowsocksRustClient = "ghcr.io/shadowsocks/sslocal-rust:latest"
 	ImageShadowsocksRustClient = "ghcr.io/shadowsocks/sslocal-rust:latest"
 	ImageV2RayCore             = "v2fly/v2fly-core:latest"
 	ImageV2RayCore             = "v2fly/v2fly-core:latest"
+	ImageTrojan                = "trojangfw/trojan:latest"
 )
 )
 
 
 var allImages = []string{
 var allImages = []string{
 	ImageShadowsocksRustServer,
 	ImageShadowsocksRustServer,
 	ImageShadowsocksRustClient,
 	ImageShadowsocksRustClient,
 	ImageV2RayCore,
 	ImageV2RayCore,
+	ImageTrojan,
 }
 }
 
 
 var localIP = netip.MustParseAddr("127.0.0.1")
 var localIP = netip.MustParseAddr("127.0.0.1")

+ 28 - 0
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-----

+ 25 - 0
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-----

+ 40 - 0
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
+    }
+}

+ 11 - 1
test/docker_test.go

@@ -3,6 +3,7 @@ package main
 import (
 import (
 	"context"
 	"context"
 	"os"
 	"os"
+	"path/filepath"
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
@@ -24,7 +25,7 @@ type DockerOptions struct {
 	Ports      []uint16
 	Ports      []uint16
 	Cmd        []string
 	Cmd        []string
 	Env        []string
 	Env        []string
-	Bind       []string
+	Bind       map[string]string
 	Stdin      []byte
 	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, "")
 	dockerContainer, err := dockerClient.ContainerCreate(context.Background(), &containerOptions, &hostOptions, nil, nil, "")
 	require.NoError(t, err)
 	require.NoError(t, err)
 	t.Cleanup(func() {
 	t.Cleanup(func() {

+ 1 - 1
test/go.mod

@@ -10,7 +10,7 @@ require (
 	github.com/docker/docker v20.10.17+incompatible
 	github.com/docker/docker v20.10.17+incompatible
 	github.com/docker/go-connections v0.4.0
 	github.com/docker/go-connections v0.4.0
 	github.com/gofrs/uuid v4.2.0+incompatible
 	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/sagernet/sing-shadowsocks v0.0.0-20220801112336-a91eacdd01e1
 	github.com/spyzhov/ajson v0.7.1
 	github.com/spyzhov/ajson v0.7.1
 	github.com/stretchr/testify v1.8.0
 	github.com/stretchr/testify v1.8.0

+ 2 - 2
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/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 h1:hE+vtsjBCCPmxkRz9jZA+CicHgVkDT6H+Av5ZzskVxs=
 github.com/sagernet/netlink v0.0.0-20220803045538-bdac49abf805/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
 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 h1:jxt2PYixIkK2i7nUGW3f+PzJagEZcbNyQddBWGuqNnw=
 github.com/sagernet/sing-dns v0.0.0-20220803121532-9e1ffb850d91/go.mod h1:T77zZdE2Cm6VqnFumrpwsq+kxYsbq+vWDhmjtdSl/oM=
 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=
 github.com/sagernet/sing-shadowsocks v0.0.0-20220801112336-a91eacdd01e1 h1:RYvOc69eSNMN0dwVugrDts41Nn7Ar/C/n/fvytvFcp4=

+ 129 - 0
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)
+}