Feature: support trojan

This commit is contained in:
Dreamacro 2020-03-19 20:26:53 +08:00
parent 230e01f078
commit b562f28c1b
5 changed files with 288 additions and 9 deletions

View File

@ -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"

View File

@ -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)
}

107
adapters/outbound/trojan.go Normal file
View File

@ -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)
}

144
component/trojan/trojan.go Normal file
View File

@ -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
}

View File

@ -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"
}