diff --git a/constant/metadata.go b/constant/metadata.go index ac676b04..2eea8335 100644 --- a/constant/metadata.go +++ b/constant/metadata.go @@ -28,6 +28,7 @@ const ( VLESS REDIR TPROXY + TROJAN TUNNEL TUN TUIC @@ -77,6 +78,8 @@ func (t Type) String() string { return "Redir" case TPROXY: return "TProxy" + case TROJAN: + return "Trojan" case TUNNEL: return "Tunnel" case TUN: @@ -115,6 +118,8 @@ func ParseType(t string) (*Type, error) { res = REDIR case "TPROXY": res = TPROXY + case "TROJAN": + res = TROJAN case "TUNNEL": res = TUNNEL case "TUN": diff --git a/docs/config.yaml b/docs/config.yaml index 934cf091..8d886055 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -1238,6 +1238,33 @@ listeners: private-key: ./server.key # padding-scheme: "" # https://github.com/anytls/anytls-go/blob/main/docs/protocol.md#cmdupdatepaddingscheme + - name: trojan-in-1 + type: trojan + port: 10819 + listen: 0.0.0.0 + # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules + # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) + users: + - username: 1 + password: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68 + # ws-path: "/" # 如果不为空则开启 websocket 传输层 + # 下面两项如果填写则开启 tls(需要同时填写) + certificate: ./server.crt + private-key: ./server.key + # 如果填写reality-config则开启reality(注意不可与certificate和private-key同时填写) + # reality-config: + # dest: test.com:443 + # private-key: jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0 # 可由 mihomo generate reality-keypair 命令生成 + # short-id: + # - 0123456789abcdef + # server-names: + # - test.com + # ss-option: # like trojan-go's `shadowsocks` config + # enabled: false + # method: aes-128-gcm # aes-128-gcm/aes-256-gcm/chacha20-ietf-poly1305 + # password: "example" + ### 注意,对于trojan listener, 至少需要填写 “certificate和private-key” 或 “reality-config” 或 “ss-option” 的其中一项 ### + - name: tun-in-1 type: tun # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules diff --git a/go.mod b/go.mod index 1eb79004..550fc050 100644 --- a/go.mod +++ b/go.mod @@ -44,6 +44,7 @@ require ( github.com/sagernet/sing v0.5.1 github.com/sagernet/sing-mux v0.2.1 github.com/sagernet/sing-shadowtls v0.1.5 + github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 github.com/samber/lo v1.49.1 github.com/shirou/gopsutil/v4 v4.25.1 github.com/sirupsen/logrus v1.9.3 @@ -98,7 +99,6 @@ require ( github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/sagernet/nftables v0.3.0-beta.4 // indirect - github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect diff --git a/listener/config/trojan.go b/listener/config/trojan.go new file mode 100644 index 00000000..637266c5 --- /dev/null +++ b/listener/config/trojan.go @@ -0,0 +1,37 @@ +package config + +import ( + "encoding/json" + + "github.com/metacubex/mihomo/listener/reality" + "github.com/metacubex/mihomo/listener/sing" +) + +type TrojanUser struct { + Username string + Password string +} + +type TrojanServer struct { + Enable bool + Listen string + Users []TrojanUser + WsPath string + Certificate string + PrivateKey string + RealityConfig reality.Config + MuxOption sing.MuxOption + TrojanSSOption TrojanSSOption +} + +// TrojanSSOption from https://github.com/p4gefau1t/trojan-go/blob/v0.10.6/tunnel/shadowsocks/config.go#L5 +type TrojanSSOption struct { + Enabled bool + Method string + Password string +} + +func (t TrojanServer) String() string { + b, _ := json.Marshal(t) + return string(b) +} diff --git a/listener/inbound/trojan.go b/listener/inbound/trojan.go new file mode 100644 index 00000000..0c5188e4 --- /dev/null +++ b/listener/inbound/trojan.go @@ -0,0 +1,108 @@ +package inbound + +import ( + C "github.com/metacubex/mihomo/constant" + LC "github.com/metacubex/mihomo/listener/config" + "github.com/metacubex/mihomo/listener/trojan" + "github.com/metacubex/mihomo/log" +) + +type TrojanOption struct { + BaseOption + Users []TrojanUser `inbound:"users"` + WsPath string `inbound:"ws-path,omitempty"` + Certificate string `inbound:"certificate,omitempty"` + PrivateKey string `inbound:"private-key,omitempty"` + RealityConfig RealityConfig `inbound:"reality-config,omitempty"` + MuxOption MuxOption `inbound:"mux-option,omitempty"` + SSOption TrojanSSOption `inbound:"ss-option,omitempty"` +} + +type TrojanUser struct { + Username string `inbound:"username,omitempty"` + Password string `inbound:"password"` +} + +// TrojanSSOption from https://github.com/p4gefau1t/trojan-go/blob/v0.10.6/tunnel/shadowsocks/config.go#L5 +type TrojanSSOption struct { + Enabled bool `inbound:"enabled,omitempty"` + Method string `inbound:"method,omitempty"` + Password string `inbound:"password,omitempty"` +} + +func (o TrojanOption) Equal(config C.InboundConfig) bool { + return optionToString(o) == optionToString(config) +} + +type Trojan struct { + *Base + config *TrojanOption + l C.MultiAddrListener + vs LC.TrojanServer +} + +func NewTrojan(options *TrojanOption) (*Trojan, error) { + base, err := NewBase(&options.BaseOption) + if err != nil { + return nil, err + } + users := make([]LC.TrojanUser, len(options.Users)) + for i, v := range options.Users { + users[i] = LC.TrojanUser{ + Username: v.Username, + Password: v.Password, + } + } + return &Trojan{ + Base: base, + config: options, + vs: LC.TrojanServer{ + Enable: true, + Listen: base.RawAddress(), + Users: users, + WsPath: options.WsPath, + Certificate: options.Certificate, + PrivateKey: options.PrivateKey, + RealityConfig: options.RealityConfig.Build(), + MuxOption: options.MuxOption.Build(), + TrojanSSOption: LC.TrojanSSOption{ + Enabled: options.SSOption.Enabled, + Method: options.SSOption.Method, + Password: options.SSOption.Password, + }, + }, + }, nil +} + +// Config implements constant.InboundListener +func (v *Trojan) Config() C.InboundConfig { + return v.config +} + +// Address implements constant.InboundListener +func (v *Trojan) Address() string { + if v.l != nil { + for _, addr := range v.l.AddrList() { + return addr.String() + } + } + return "" +} + +// Listen implements constant.InboundListener +func (v *Trojan) Listen(tunnel C.Tunnel) error { + var err error + v.l, err = trojan.New(v.vs, tunnel, v.Additions()...) + if err != nil { + return err + } + log.Infoln("Trojan[%s] proxy listening at: %s", v.Name(), v.Address()) + return nil +} + +// Close implements constant.InboundListener +func (v *Trojan) Close() error { + return v.l.Close() +} + +var _ C.InboundListener = (*Trojan)(nil) diff --git a/listener/inbound/vless.go b/listener/inbound/vless.go index eb3b3c5a..82499824 100644 --- a/listener/inbound/vless.go +++ b/listener/inbound/vless.go @@ -81,14 +81,6 @@ func (v *Vless) Address() string { // Listen implements constant.InboundListener func (v *Vless) Listen(tunnel C.Tunnel) error { var err error - users := make([]LC.VlessUser, len(v.config.Users)) - for i, v := range v.config.Users { - users[i] = LC.VlessUser{ - Username: v.Username, - UUID: v.UUID, - Flow: v.Flow, - } - } v.l, err = sing_vless.New(v.vs, tunnel, v.Additions()...) if err != nil { return err diff --git a/listener/inbound/vmess.go b/listener/inbound/vmess.go index cf2379e1..151e2d7a 100644 --- a/listener/inbound/vmess.go +++ b/listener/inbound/vmess.go @@ -81,14 +81,6 @@ func (v *Vmess) Address() string { // Listen implements constant.InboundListener func (v *Vmess) Listen(tunnel C.Tunnel) error { var err error - users := make([]LC.VmessUser, len(v.config.Users)) - for i, v := range v.config.Users { - users[i] = LC.VmessUser{ - Username: v.Username, - UUID: v.UUID, - AlterID: v.AlterID, - } - } v.l, err = sing_vmess.New(v.vs, tunnel, v.Additions()...) if err != nil { return err diff --git a/listener/parse.go b/listener/parse.go index 5c5d6c7e..adc206c1 100644 --- a/listener/parse.go +++ b/listener/parse.go @@ -93,6 +93,13 @@ func ParseListener(mapping map[string]any) (C.InboundListener, error) { return nil, err } listener, err = IN.NewVless(vlessOption) + case "trojan": + trojanOption := &IN.TrojanOption{} + err = decoder.Decode(mapping, trojanOption) + if err != nil { + return nil, err + } + listener, err = IN.NewTrojan(trojanOption) case "hysteria2": hysteria2Option := &IN.Hysteria2Option{} err = decoder.Decode(mapping, hysteria2Option) diff --git a/listener/trojan/packet.go b/listener/trojan/packet.go new file mode 100644 index 00000000..5111242e --- /dev/null +++ b/listener/trojan/packet.go @@ -0,0 +1,43 @@ +package trojan + +import ( + "errors" + "net" +) + +type packet struct { + pc net.PacketConn + rAddr net.Addr + payload []byte + put func() +} + +func (c *packet) Data() []byte { + return c.payload +} + +// WriteBack wirtes UDP packet with source(ip, port) = `addr` +func (c *packet) WriteBack(b []byte, addr net.Addr) (n int, err error) { + if addr == nil { + err = errors.New("address is invalid") + return + } + return c.pc.WriteTo(b, addr) +} + +// LocalAddr returns the source IP/Port of UDP Packet +func (c *packet) LocalAddr() net.Addr { + return c.rAddr +} + +func (c *packet) Drop() { + if c.put != nil { + c.put() + c.put = nil + } + c.payload = nil +} + +func (c *packet) InAddr() net.Addr { + return c.pc.LocalAddr() +} diff --git a/listener/trojan/server.go b/listener/trojan/server.go new file mode 100644 index 00000000..76177882 --- /dev/null +++ b/listener/trojan/server.go @@ -0,0 +1,271 @@ +package trojan + +import ( + "crypto/tls" + "errors" + "io" + "net" + "net/http" + "strings" + + "github.com/metacubex/mihomo/adapter/inbound" + N "github.com/metacubex/mihomo/common/net" + C "github.com/metacubex/mihomo/constant" + LC "github.com/metacubex/mihomo/listener/config" + "github.com/metacubex/mihomo/listener/reality" + "github.com/metacubex/mihomo/listener/sing" + "github.com/metacubex/mihomo/transport/shadowsocks/core" + "github.com/metacubex/mihomo/transport/socks5" + "github.com/metacubex/mihomo/transport/trojan" + mihomoVMess "github.com/metacubex/mihomo/transport/vmess" + + "github.com/sagernet/smux" +) + +type Listener struct { + closed bool + config LC.TrojanServer + listeners []net.Listener + keys map[[trojan.KeyLength]byte]string + pickCipher core.Cipher + handler *sing.ListenerHandler +} + +func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition) (sl *Listener, err error) { + if len(additions) == 0 { + additions = []inbound.Addition{ + inbound.WithInName("DEFAULT-TROJAN"), + inbound.WithSpecialRules(""), + } + } + h, err := sing.NewListenerHandler(sing.ListenerConfig{ + Tunnel: tunnel, + Type: C.TROJAN, + Additions: additions, + MuxOption: config.MuxOption, + }) + if err != nil { + return nil, err + } + + keys := make(map[[trojan.KeyLength]byte]string) + for _, user := range config.Users { + keys[trojan.Key(user.Password)] = user.Username + } + + var pickCipher core.Cipher + if config.TrojanSSOption.Enabled { + if config.TrojanSSOption.Password == "" { + return nil, errors.New("empty password") + } + if config.TrojanSSOption.Method == "" { + config.TrojanSSOption.Method = "AES-128-GCM" + } + pickCipher, err = core.PickCipher(config.TrojanSSOption.Method, nil, config.TrojanSSOption.Password) + if err != nil { + return nil, err + } + } + sl = &Listener{false, config, nil, keys, pickCipher, h} + + tlsConfig := &tls.Config{} + var realityBuilder *reality.Builder + var httpMux *http.ServeMux + + if config.Certificate != "" && config.PrivateKey != "" { + cert, err := N.ParseCert(config.Certificate, config.PrivateKey, C.Path) + if err != nil { + return nil, err + } + tlsConfig.Certificates = []tls.Certificate{cert} + } + if config.RealityConfig.PrivateKey != "" { + if tlsConfig.Certificates != nil { + return nil, errors.New("certificate is unavailable in reality") + } + realityBuilder, err = config.RealityConfig.Build() + if err != nil { + return nil, err + } + } + if config.WsPath != "" { + httpMux = http.NewServeMux() + httpMux.HandleFunc(config.WsPath, func(w http.ResponseWriter, r *http.Request) { + conn, err := mihomoVMess.StreamUpgradedWebsocketConn(w, r) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + sl.HandleConn(conn, tunnel) + }) + tlsConfig.NextProtos = append(tlsConfig.NextProtos, "http/1.1") + } + + for _, addr := range strings.Split(config.Listen, ",") { + addr := addr + + //TCP + l, err := inbound.Listen("tcp", addr) + if err != nil { + return nil, err + } + if realityBuilder != nil { + l = realityBuilder.NewListener(l) + } else if len(tlsConfig.Certificates) > 0 { + l = tls.NewListener(l, tlsConfig) + } else if !config.TrojanSSOption.Enabled { + return nil, errors.New("disallow using Trojan without both certificates/reality/ss config") + } + sl.listeners = append(sl.listeners, l) + + go func() { + if httpMux != nil { + _ = http.Serve(l, httpMux) + return + } + for { + c, err := l.Accept() + if err != nil { + if sl.closed { + break + } + continue + } + + go sl.HandleConn(c, tunnel) + } + }() + } + + return sl, nil +} + +func (l *Listener) Close() error { + l.closed = true + var retErr error + for _, lis := range l.listeners { + err := lis.Close() + if err != nil { + retErr = err + } + } + return retErr +} + +func (l *Listener) Config() string { + return l.config.String() +} + +func (l *Listener) AddrList() (addrList []net.Addr) { + for _, lis := range l.listeners { + addrList = append(addrList, lis.Addr()) + } + return +} + +func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { + defer conn.Close() + + if l.pickCipher != nil { + conn = l.pickCipher.StreamConn(conn) + } + + var key [trojan.KeyLength]byte + if _, err := io.ReadFull(conn, key[:]); err != nil { + //log.Warnln("read key error: %s", err.Error()) + return + } + + if user, ok := l.keys[key]; ok { + additions = append(additions, inbound.WithInUser(user)) + } else { + //log.Warnln("no such key") + return + } + + var crlf [2]byte + if _, err := io.ReadFull(conn, crlf[:]); err != nil { + //log.Warnln("read crlf error: %s", err.Error()) + return + } + + l.handleConn(false, conn, tunnel, additions...) +} + +func (l *Listener) handleConn(inMux bool, conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { + if inMux { + defer conn.Close() + } + + command, err := socks5.ReadByte(conn) + if err != nil { + //log.Warnln("read command error: %s", err.Error()) + return + } + + switch command { + case trojan.CommandTCP, trojan.CommandUDP, trojan.CommandMux: + default: + //log.Warnln("unknown command: %d", command) + return + } + + target, err := socks5.ReadAddr0(conn) + if err != nil { + //log.Warnln("read target error: %s", err.Error()) + return + } + + if !inMux { + var crlf [2]byte + if _, err := io.ReadFull(conn, crlf[:]); err != nil { + //log.Warnln("read crlf error: %s", err.Error()) + return + } + } + + switch command { + case trojan.CommandTCP: + //tunnel.HandleTCPConn(inbound.NewSocket(target, conn, C.TROJAN, additions...)) + l.handler.HandleSocket(target, conn, additions...) + case trojan.CommandUDP: + pc := trojan.NewPacketConn(conn) + for { + data, put, remoteAddr, err := pc.WaitReadFrom() + if err != nil { + if put != nil { + put() + } + break + } + cPacket := &packet{ + pc: pc, + rAddr: remoteAddr, + payload: data, + put: put, + } + + tunnel.HandleUDPPacket(inbound.NewPacket(target, cPacket, C.TROJAN, additions...)) + } + case trojan.CommandMux: + if inMux { + //log.Warnln("invalid command: %d", command) + return + } + smuxConfig := smux.DefaultConfig() + smuxConfig.KeepAliveDisabled = true + session, err := smux.Server(conn, smuxConfig) + if err != nil { + //log.Warnln("smux server error: %s", err.Error()) + return + } + defer session.Close() + for { + stream, err := session.AcceptStream() + if err != nil { + return + } + go l.handleConn(true, stream, tunnel, additions...) + } + } +} diff --git a/transport/trojan/trojan.go b/transport/trojan/trojan.go index c1ebb8da..e5000502 100644 --- a/transport/trojan/trojan.go +++ b/transport/trojan/trojan.go @@ -38,10 +38,9 @@ type Command = byte const ( CommandTCP byte = 1 CommandUDP byte = 3 + CommandMux byte = 0x7f - // deprecated XTLS commands, as souvenirs - commandXRD byte = 0xf0 // XTLS direct mode - commandXRO byte = 0xf1 // XTLS origin mode + KeyLength = 56 ) type Option struct { @@ -65,7 +64,7 @@ type WebsocketOption struct { type Trojan struct { option *Option - hexPassword []byte + hexPassword [KeyLength]byte } func (t *Trojan) StreamConn(ctx context.Context, conn net.Conn) (net.Conn, error) { @@ -152,7 +151,7 @@ func (t *Trojan) WriteHeader(w io.Writer, command Command, socks5Addr []byte) er buf := pool.GetBuffer() defer pool.PutBuffer(buf) - buf.Write(t.hexPassword) + buf.Write(t.hexPassword[:]) buf.Write(crlf) buf.WriteByte(command) @@ -245,7 +244,7 @@ func ReadPacket(r io.Reader, payload []byte) (net.Addr, int, int, error) { } func New(option *Option) *Trojan { - return &Trojan{option, hexSha224([]byte(option.Password))} + return &Trojan{option, Key(option.Password)} } var _ N.EnhancePacketConn = (*PacketConn)(nil) @@ -340,9 +339,12 @@ func (pc *PacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, er return } -func hexSha224(data []byte) []byte { - buf := make([]byte, 56) - hash := sha256.Sum224(data) - hex.Encode(buf, hash[:]) - return buf +func NewPacketConn(conn net.Conn) *PacketConn { + return &PacketConn{Conn: conn} +} + +func Key(password string) (key [56]byte) { + hash := sha256.Sum224([]byte(password)) + hex.Encode(key[:], hash[:]) + return }