From 55ee8695b3611a764f98e66cdd824db80617829f Mon Sep 17 00:00:00 2001 From: gVisor bot Date: Thu, 19 Mar 2020 20:26:53 +0800 Subject: [PATCH] Feature: support trojan --- README.md | 13 ++++ adapters/outbound/parser.go | 7 ++ adapters/outbound/trojan.go | 107 +++++++++++++++++++++++++++ component/trojan/trojan.go | 144 ++++++++++++++++++++++++++++++++++++ constant/adapters.go | 26 ++++--- 5 files changed, 288 insertions(+), 9 deletions(-) create mode 100644 adapters/outbound/trojan.go create mode 100644 component/trojan/trojan.go diff --git a/README.md b/README.md index 2f7aba25..a67113f7 100644 --- a/README.md +++ b/README.md @@ -243,6 +243,19 @@ proxies: # mode: http # or tls # host: bing.com + # trojan + - name: "trojan" + type: trojan + server: server + port: 443 + password: yourpsk + # udp: true + # sni: example.com # aka server name + # alpn: + # - h2 + # - http/1.1 + # skip-cert-verify: true + proxy-groups: # url-test select which proxy will be used by benchmarking speed to a URL. - name: "auto" diff --git a/adapters/outbound/parser.go b/adapters/outbound/parser.go index 6aeec866..90b77760 100644 --- a/adapters/outbound/parser.go +++ b/adapters/outbound/parser.go @@ -52,6 +52,13 @@ func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) { break } proxy, err = NewSnell(*snellOption) + case "trojan": + trojanOption := &TrojanOption{} + err = decoder.Decode(mapping, trojanOption) + if err != nil { + break + } + proxy, err = NewTrojan(*trojanOption) default: return nil, fmt.Errorf("Unsupport proxy type: %s", proxyType) } diff --git a/adapters/outbound/trojan.go b/adapters/outbound/trojan.go new file mode 100644 index 00000000..14564bc0 --- /dev/null +++ b/adapters/outbound/trojan.go @@ -0,0 +1,107 @@ +package outbound + +import ( + "context" + "encoding/json" + "fmt" + "net" + "strconv" + + "github.com/Dreamacro/clash/component/dialer" + "github.com/Dreamacro/clash/component/trojan" + C "github.com/Dreamacro/clash/constant" +) + +type Trojan struct { + *Base + server string + instance *trojan.Trojan +} + +type TrojanOption struct { + Name string `proxy:"name"` + Server string `proxy:"server"` + Port int `proxy:"port"` + Password string `proxy:"password"` + ALPN []string `proxy:"alpn,omitempty"` + SNI string `proxy:"sni,omitempty"` + SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` + UDP bool `proxy:"udp,omitempty"` +} + +func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { + c, err := dialer.DialContext(ctx, "tcp", t.server) + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", t.server, err) + } + tcpKeepAlive(c) + c, err = t.instance.StreamConn(c) + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", t.server, err) + } + + err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)) + return newConn(c, t), err +} + +func (t *Trojan) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { + ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout) + defer cancel() + c, err := dialer.DialContext(ctx, "tcp", t.server) + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", t.server, err) + } + tcpKeepAlive(c) + c, err = t.instance.StreamConn(c) + if err != nil { + return nil, fmt.Errorf("%s connect error: %w", t.server, err) + } + + err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata)) + if err != nil { + return nil, err + } + + pc := t.instance.PacketConn(c) + return newPacketConn(&trojanPacketConn{pc, c}, t), err +} + +func (t *Trojan) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]string{ + "type": t.Type().String(), + }) +} + +func NewTrojan(option TrojanOption) (*Trojan, error) { + server := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) + + tOption := &trojan.Option{ + Password: option.Password, + ALPN: option.ALPN, + ServerName: option.Server, + SkipCertVerify: option.SkipCertVerify, + } + + if option.SNI != "" { + tOption.ServerName = option.SNI + } + + return &Trojan{ + Base: &Base{ + name: option.Name, + tp: C.Trojan, + udp: option.UDP, + }, + server: server, + instance: trojan.New(tOption), + }, nil +} + +type trojanPacketConn struct { + net.PacketConn + conn net.Conn +} + +func (tpc *trojanPacketConn) WriteWithMetadata(p []byte, metadata *C.Metadata) (n int, err error) { + return trojan.WritePacket(tpc.conn, serializesSocksAddr(metadata), p) +} diff --git a/component/trojan/trojan.go b/component/trojan/trojan.go new file mode 100644 index 00000000..b20fa3ae --- /dev/null +++ b/component/trojan/trojan.go @@ -0,0 +1,144 @@ +package trojan + +import ( + "bytes" + "crypto/sha256" + "crypto/tls" + "encoding/binary" + "encoding/hex" + "errors" + "net" + "sync" + + "github.com/Dreamacro/clash/component/socks5" +) + +var ( + defaultALPN = []string{"h2", "http/1.1"} + crlf = []byte{'\r', '\n'} + + bufPool = sync.Pool{New: func() interface{} { return &bytes.Buffer{} }} +) + +type Command = byte + +var ( + CommandTCP byte = 1 + CommandUDP byte = 3 +) + +type Option struct { + Password string + ALPN []string + ServerName string + SkipCertVerify bool +} + +type Trojan struct { + option *Option + hexPassword []byte +} + +func (t *Trojan) StreamConn(conn net.Conn) (net.Conn, error) { + alpn := defaultALPN + if len(t.option.ALPN) != 0 { + alpn = t.option.ALPN + } + + tlsConfig := &tls.Config{ + NextProtos: alpn, + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: t.option.SkipCertVerify, + ServerName: t.option.ServerName, + } + + tlsConn := tls.Client(conn, tlsConfig) + if err := tlsConn.Handshake(); err != nil { + return nil, err + } + + return tlsConn, nil +} + +func (t *Trojan) WriteHeader(conn net.Conn, command Command, socks5Addr []byte) error { + buf := bufPool.Get().(*bytes.Buffer) + defer buf.Reset() + defer bufPool.Put(buf) + + buf.Write(t.hexPassword) + buf.Write(crlf) + + buf.WriteByte(command) + buf.Write(socks5Addr) + buf.Write(crlf) + + _, err := conn.Write(buf.Bytes()) + return err +} + +func (t *Trojan) PacketConn(conn net.Conn) net.PacketConn { + return &PacketConn{conn} +} + +func WritePacket(conn net.Conn, socks5Addr, payload []byte) (int, error) { + buf := bufPool.Get().(*bytes.Buffer) + defer buf.Reset() + defer bufPool.Put(buf) + + buf.Write(socks5Addr) + binary.Write(buf, binary.BigEndian, uint16(len(payload))) + buf.Write(crlf) + buf.Write(payload) + + return conn.Write(buf.Bytes()) +} + +func DecodePacket(payload []byte) (net.Addr, []byte, error) { + addr := socks5.SplitAddr(payload) + if addr == nil { + return nil, nil, errors.New("split addr error") + } + + buf := payload[len(addr):] + if len(buf) <= 4 { + return nil, nil, errors.New("packet invalid") + } + + length := binary.BigEndian.Uint16(buf[:2]) + if len(buf) < 4+int(length) { + return nil, nil, errors.New("packet invalid") + } + + return addr.UDPAddr(), buf[4 : 4+length], nil +} + +func New(option *Option) *Trojan { + return &Trojan{option, hexSha224([]byte(option.Password))} +} + +type PacketConn struct { + net.Conn +} + +func (pc *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { + return WritePacket(pc, socks5.ParseAddr(addr.String()), b) +} + +func (pc *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) { + n, err := pc.Conn.Read(b) + addr, payload, err := DecodePacket(b) + if err != nil { + return n, nil, err + } + + copy(b, payload) + return len(payload), addr, nil +} + +func hexSha224(data []byte) []byte { + buf := make([]byte, 56) + hash := sha256.New224() + hash.Write(data) + hex.Encode(buf, hash.Sum(nil)) + return buf +} diff --git a/constant/adapters.go b/constant/adapters.go index 31db23b5..3bc6d278 100644 --- a/constant/adapters.go +++ b/constant/adapters.go @@ -10,15 +10,18 @@ import ( // Adapter Type const ( Direct AdapterType = iota - Fallback Reject - Selector + Shadowsocks Snell Socks5 Http - URLTest Vmess + Trojan + + Selector + Fallback + URLTest LoadBalance ) @@ -86,12 +89,9 @@ func (at AdapterType) String() string { switch at { case Direct: return "Direct" - case Fallback: - return "Fallback" case Reject: return "Reject" - case Selector: - return "Selector" + case Shadowsocks: return "Shadowsocks" case Snell: @@ -100,12 +100,20 @@ func (at AdapterType) String() string { return "Socks5" case Http: return "Http" - case URLTest: - return "URLTest" case Vmess: return "Vmess" + case Trojan: + return "Trojan" + + case Selector: + return "Selector" + case Fallback: + return "Fallback" + case URLTest: + return "URLTest" case LoadBalance: return "LoadBalance" + default: return "Unknown" }