From e0956ce58cac936c212b3f399f3d210ec8daa828 Mon Sep 17 00:00:00 2001 From: xjasonlyu Date: Sat, 6 Feb 2021 13:41:44 +0800 Subject: [PATCH] Feature: add simple-obfs support for ss --- component/simple-obfs/http.go | 94 ++++++++++++++++ component/simple-obfs/obfs.go | 4 + component/simple-obfs/tls.go | 198 ++++++++++++++++++++++++++++++++++ engine/parse.go | 51 +++++++-- proxy/shadowsocks.go | 20 +++- 5 files changed, 357 insertions(+), 10 deletions(-) create mode 100644 component/simple-obfs/http.go create mode 100644 component/simple-obfs/obfs.go create mode 100644 component/simple-obfs/tls.go diff --git a/component/simple-obfs/http.go b/component/simple-obfs/http.go new file mode 100644 index 0000000..9b7eebc --- /dev/null +++ b/component/simple-obfs/http.go @@ -0,0 +1,94 @@ +package obfs + +import ( + "bytes" + "encoding/base64" + "fmt" + "io" + "math/rand" + "net" + "net/http" + + "github.com/xjasonlyu/tun2socks/common/pool" +) + +// HTTPObfs is shadowsocks http simple-obfs implementation +type HTTPObfs struct { + net.Conn + host string + port string + buf []byte + offset int + firstRequest bool + firstResponse bool +} + +func (ho *HTTPObfs) Read(b []byte) (int, error) { + if ho.buf != nil { + n := copy(b, ho.buf[ho.offset:]) + ho.offset += n + if ho.offset == len(ho.buf) { + pool.Put(ho.buf) + ho.buf = nil + } + return n, nil + } + + if ho.firstResponse { + buf := pool.Get(pool.RelayBufferSize) + n, err := ho.Conn.Read(buf) + if err != nil { + pool.Put(buf) + return 0, err + } + idx := bytes.Index(buf[:n], []byte("\r\n\r\n")) + if idx == -1 { + pool.Put(buf) + return 0, io.EOF + } + ho.firstResponse = false + length := n - (idx + 4) + n = copy(b, buf[idx+4:n]) + if length > n { + ho.buf = buf[:idx+4+length] + ho.offset = idx + 4 + n + } else { + pool.Put(buf) + } + return n, nil + } + return ho.Conn.Read(b) +} + +func (ho *HTTPObfs) Write(b []byte) (int, error) { + if ho.firstRequest { + randBytes := make([]byte, 16) + rand.Read(randBytes) + req, _ := http.NewRequest("GET", fmt.Sprintf("http://%s/", ho.host), bytes.NewBuffer(b[:])) + req.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", rand.Int()%54, rand.Int()%2)) + req.Header.Set("Upgrade", "websocket") + req.Header.Set("Connection", "Upgrade") + req.Host = ho.host + if ho.port != "80" { + req.Host = fmt.Sprintf("%s:%s", ho.host, ho.port) + } + req.Header.Set("Sec-WebSocket-Key", base64.URLEncoding.EncodeToString(randBytes)) + req.ContentLength = int64(len(b)) + err := req.Write(ho.Conn) + ho.firstRequest = false + return len(b), err + } + + return ho.Conn.Write(b) +} + +// NewHTTPObfs return a HTTPObfs +func NewHTTPObfs(conn net.Conn, host string, port string) net.Conn { + return &HTTPObfs{ + Conn: conn, + firstRequest: true, + firstResponse: true, + host: host, + port: port, + } +} diff --git a/component/simple-obfs/obfs.go b/component/simple-obfs/obfs.go new file mode 100644 index 0000000..f041d57 --- /dev/null +++ b/component/simple-obfs/obfs.go @@ -0,0 +1,4 @@ +// Package obfs provides obfuscation functionality for Shadowsocks protocol. +package obfs + +// Ref: github.com/Dreamacro/clash/component/simple-obfs diff --git a/component/simple-obfs/tls.go b/component/simple-obfs/tls.go new file mode 100644 index 0000000..7f57baf --- /dev/null +++ b/component/simple-obfs/tls.go @@ -0,0 +1,198 @@ +package obfs + +import ( + "bytes" + "encoding/binary" + "io" + "math/rand" + "net" + "time" + + "github.com/xjasonlyu/tun2socks/common/pool" +) + +func init() { + rand.Seed(time.Now().Unix()) +} + +const ( + chunkSize = 1 << 14 // 2 ** 14 == 16 * 1024 +) + +// TLSObfs is shadowsocks tls simple-obfs implementation +type TLSObfs struct { + net.Conn + server string + remain int + firstRequest bool + firstResponse bool +} + +func (to *TLSObfs) read(b []byte, discardN int) (int, error) { + buf := pool.Get(discardN) + _, err := io.ReadFull(to.Conn, buf) + if err != nil { + return 0, err + } + pool.Put(buf) + + sizeBuf := make([]byte, 2) + _, err = io.ReadFull(to.Conn, sizeBuf) + if err != nil { + return 0, nil + } + + length := int(binary.BigEndian.Uint16(sizeBuf)) + if length > len(b) { + n, err := to.Conn.Read(b) + if err != nil { + return n, err + } + to.remain = length - n + return n, nil + } + + return io.ReadFull(to.Conn, b[:length]) +} + +func (to *TLSObfs) Read(b []byte) (int, error) { + if to.remain > 0 { + length := to.remain + if length > len(b) { + length = len(b) + } + + n, err := io.ReadFull(to.Conn, b[:length]) + to.remain -= n + return n, err + } + + if to.firstResponse { + // type + ver + len_size + 91 = 96 + // type + ver + len_size + 1 = 6 + // type + ver = 3 + to.firstResponse = false + return to.read(b, 105) + } + + // type + ver = 3 + return to.read(b, 3) +} +func (to *TLSObfs) Write(b []byte) (int, error) { + length := len(b) + for i := 0; i < length; i += chunkSize { + end := i + chunkSize + if end > length { + end = length + } + + n, err := to.write(b[i:end]) + if err != nil { + return n, err + } + } + return length, nil +} + +func (to *TLSObfs) write(b []byte) (int, error) { + if to.firstRequest { + helloMsg := makeClientHelloMsg(b, to.server) + _, err := to.Conn.Write(helloMsg) + to.firstRequest = false + return len(b), err + } + + buf := &bytes.Buffer{} + buf.Write([]byte{0x17, 0x03, 0x03}) + binary.Write(buf, binary.BigEndian, uint16(len(b))) + buf.Write(b) + _, err := to.Conn.Write(buf.Bytes()) + return len(b), err +} + +// NewTLSObfs return a SimpleObfs +func NewTLSObfs(conn net.Conn, server string) net.Conn { + return &TLSObfs{ + Conn: conn, + server: server, + firstRequest: true, + firstResponse: true, + } +} + +func makeClientHelloMsg(data []byte, server string) []byte { + random := make([]byte, 28) + sessionID := make([]byte, 32) + rand.Read(random) + rand.Read(sessionID) + + buf := &bytes.Buffer{} + + // handshake, TLS 1.0 version, length + buf.WriteByte(22) + buf.Write([]byte{0x03, 0x01}) + length := uint16(212 + len(data) + len(server)) + buf.WriteByte(byte(length >> 8)) + buf.WriteByte(byte(length & 0xff)) + + // clientHello, length, TLS 1.2 version + buf.WriteByte(1) + buf.WriteByte(0) + binary.Write(buf, binary.BigEndian, uint16(208+len(data)+len(server))) + buf.Write([]byte{0x03, 0x03}) + + // random with timestamp, sid len, sid + binary.Write(buf, binary.BigEndian, uint32(time.Now().Unix())) + buf.Write(random) + buf.WriteByte(32) + buf.Write(sessionID) + + // cipher suites + buf.Write([]byte{0x00, 0x38}) + buf.Write([]byte{ + 0xc0, 0x2c, 0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f, + 0x00, 0x9e, 0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23, 0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a, + 0xc0, 0x14, 0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33, 0x00, 0x9d, 0x00, 0x9c, 0x00, 0x3d, + 0x00, 0x3c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0xff, + }) + + // compression + buf.Write([]byte{0x01, 0x00}) + + // extension length + binary.Write(buf, binary.BigEndian, uint16(79+len(data)+len(server))) + + // session ticket + buf.Write([]byte{0x00, 0x23}) + binary.Write(buf, binary.BigEndian, uint16(len(data))) + buf.Write(data) + + // server name + buf.Write([]byte{0x00, 0x00}) + binary.Write(buf, binary.BigEndian, uint16(len(server)+5)) + binary.Write(buf, binary.BigEndian, uint16(len(server)+3)) + buf.WriteByte(0) + binary.Write(buf, binary.BigEndian, uint16(len(server))) + buf.Write([]byte(server)) + + // ec_point + buf.Write([]byte{0x00, 0x0b, 0x00, 0x04, 0x03, 0x01, 0x00, 0x02}) + + // groups + buf.Write([]byte{0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, 0x00, 0x18}) + + // signature + buf.Write([]byte{ + 0x00, 0x0d, 0x00, 0x20, 0x00, 0x1e, 0x06, 0x01, 0x06, 0x02, 0x06, 0x03, 0x05, + 0x01, 0x05, 0x02, 0x05, 0x03, 0x04, 0x01, 0x04, 0x02, 0x04, 0x03, 0x03, 0x01, + 0x03, 0x02, 0x03, 0x03, 0x02, 0x01, 0x02, 0x02, 0x02, 0x03, + }) + + // encrypt then mac + buf.Write([]byte{0x00, 0x16, 0x00, 0x00}) + + // extended master secret + buf.Write([]byte{0x00, 0x17, 0x00, 0x00}) + + return buf.Bytes() +} diff --git a/engine/parse.go b/engine/parse.go index b65bc19..ba109a4 100644 --- a/engine/parse.go +++ b/engine/parse.go @@ -1,6 +1,7 @@ package engine import ( + "encoding/base64" "fmt" "net/url" "strings" @@ -50,20 +51,56 @@ func parseProxy(s string) (proxy.Dialer, error) { return nil, err } - addr := u.Host - user := u.User.Username() - pass, _ := u.User.Password() proto := strings.ToLower(u.Scheme) switch proto { case "direct": return proxy.NewDirect(), nil case "socks5": - return proxy.NewSocks5(addr, user, pass) - case "ss", "shadowsocks": - method, password := user, pass - return proxy.NewShadowSocks(addr, method, password) + return proxy.NewSocks5(parseSocks(u)) + case "ss": + return proxy.NewShadowSocks(parseShadowSocks(u)) } return nil, fmt.Errorf("unsupported protocol: %s", proto) } + +func parseSocks(u *url.URL) (address, username, password string) { + address = u.Host + username = u.User.Username() + password, _ = u.User.Password() + return +} + +func parseShadowSocks(u *url.URL) (address, method, password, obfsMode, obfsHost string) { + address = u.Host + + if pass, set := u.User.Password(); set { + method = u.User.Username() + password = pass + } else { + data, _ := base64.RawURLEncoding.DecodeString(u.User.String()) + userInfo := strings.SplitN(string(data), ":", 2) + method = userInfo[0] + password = userInfo[1] + } + + rawQuery, _ := url.QueryUnescape(u.RawQuery) + for _, s := range strings.Split(rawQuery, ";") { + data := strings.SplitN(s, "=", 2) + if len(data) != 2 { + continue + } + key := data[0] + value := data[1] + + switch key { + case "obfs": + obfsMode = value + case "obfs-host": + obfsHost = value + } + } + + return +} diff --git a/proxy/shadowsocks.go b/proxy/shadowsocks.go index 559faf5..fb424a9 100755 --- a/proxy/shadowsocks.go +++ b/proxy/shadowsocks.go @@ -8,6 +8,7 @@ import ( "github.com/xjasonlyu/tun2socks/common/adapter" "github.com/xjasonlyu/tun2socks/component/dialer" + obfs "github.com/xjasonlyu/tun2socks/component/simple-obfs" "github.com/xjasonlyu/tun2socks/component/socks5" "github.com/Dreamacro/go-shadowsocks2/core" @@ -17,17 +18,22 @@ type ShadowSocks struct { *Base cipher core.Cipher + + // simple-obfs plugin + obfsMode, obfsHost string } -func NewShadowSocks(addr, method, password string) (*ShadowSocks, error) { +func NewShadowSocks(addr, method, password, obfsMode, obfsHost string) (*ShadowSocks, error) { cipher, err := core.PickCipher(method, nil, password) if err != nil { return nil, fmt.Errorf("ss initialize: %w", err) } return &ShadowSocks{ - Base: NewBase(addr), - cipher: cipher, + Base: NewBase(addr), + cipher: cipher, + obfsMode: obfsMode, + obfsHost: obfsHost, }, nil } @@ -44,6 +50,14 @@ func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *adapter.Metada } }() + switch ss.obfsMode { + case "tls": + c = obfs.NewTLSObfs(c, ss.obfsHost) + case "http": + _, port, _ := net.SplitHostPort(ss.addr) + c = obfs.NewHTTPObfs(c, ss.obfsHost, port) + } + c = ss.cipher.StreamConn(c) _, err = c.Write(metadata.SerializesSocksAddr()) return