From ce13b2a82a39e4add2836dba42fe52a3c9eec252 Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Tue, 6 Jul 2021 09:53:39 +0800 Subject: [PATCH] add obfs http/tls listener --- go.mod | 2 + go.sum | 4 + server/listener/obfs/http/conn.go | 140 ++++++++++++ server/listener/obfs/http/listener.go | 88 ++++++++ server/listener/obfs/http/metadata.go | 19 ++ server/listener/obfs/tls/conn.go | 306 ++++++++++++++++++++++++++ server/listener/obfs/tls/listener.go | 88 ++++++++ server/listener/obfs/tls/metadata.go | 19 ++ 8 files changed, 666 insertions(+) create mode 100644 server/listener/obfs/http/conn.go create mode 100644 server/listener/obfs/http/listener.go create mode 100644 server/listener/obfs/http/metadata.go create mode 100644 server/listener/obfs/tls/conn.go create mode 100644 server/listener/obfs/tls/listener.go create mode 100644 server/listener/obfs/tls/metadata.go diff --git a/go.mod b/go.mod index 4c90359..a24d702 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.16 require ( github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/coreos/go-iptables v0.5.0 // indirect + github.com/ginuerzh/tls-dissector v0.0.2-0.20201202075250-98fa925912da github.com/go-gost/gosocks5 v0.3.0 github.com/golang/snappy v0.0.3 github.com/google/gopacket v1.1.19 // indirect @@ -17,4 +18,5 @@ require ( github.com/xtaci/smux v1.5.15 github.com/xtaci/tcpraw v1.2.25 golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 + golang.org/x/net v0.0.0-20200707034311-ab3426394381 ) diff --git a/go.sum b/go.sum index f1bedde..6517e5f 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,10 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ginuerzh/tls-dissector v0.0.1 h1:yF6fIt78TO4CdjiLLn6R8r0XajQJE1Lbnuq6rP8mGW8= +github.com/ginuerzh/tls-dissector v0.0.1/go.mod h1:u/kbBOqIOgJv39gywuUb3VwyzdZG5DKquOqfToKE6lk= +github.com/ginuerzh/tls-dissector v0.0.2-0.20201202075250-98fa925912da h1:CPNdzkS5TMPghHVTYJp08SUdSneNVSwJSePAPGDuYgY= +github.com/ginuerzh/tls-dissector v0.0.2-0.20201202075250-98fa925912da/go.mod h1:YyzP8PQrGwDH/XsfHJXwqdHLwWvBYxu77YVKm0+68f0= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-gost/gosocks5 v0.3.0 h1:Hkmp9YDRBSCJd7xywW6dBPT6B9aQTkuWd+3WCheJiJA= diff --git a/server/listener/obfs/http/conn.go b/server/listener/obfs/http/conn.go new file mode 100644 index 0000000..f006a97 --- /dev/null +++ b/server/listener/obfs/http/conn.go @@ -0,0 +1,140 @@ +package http + +import ( + "bufio" + "bytes" + "crypto/sha1" + "encoding/base64" + "errors" + "fmt" + "io" + "net" + "net/http" + "sync" + "time" +) + +type conn struct { + net.Conn + rbuf bytes.Buffer + wbuf bytes.Buffer + handshaked bool + handshakeMutex sync.Mutex +} + +func (c *conn) Handshake() (err error) { + c.handshakeMutex.Lock() + defer c.handshakeMutex.Unlock() + + if c.handshaked { + return nil + } + + if err = c.handshake(); err != nil { + return + } + + c.handshaked = true + return nil +} + +func (c *conn) handshake() (err error) { + br := bufio.NewReader(c.Conn) + r, err := http.ReadRequest(br) + if err != nil { + return + } + /* + if Debug { + dump, _ := httputil.DumpRequest(r, false) + log.Logf("[ohttp] %s -> %s\n%s", c.RemoteAddr(), c.LocalAddr(), string(dump)) + } + */ + + if r.ContentLength > 0 { + _, err = io.Copy(&c.rbuf, r.Body) + } else { + var b []byte + b, err = br.Peek(br.Buffered()) + if len(b) > 0 { + _, err = c.rbuf.Write(b) + } + } + if err != nil { + // log.Logf("[ohttp] %s -> %s : %v", c.Conn.RemoteAddr(), c.Conn.LocalAddr(), err) + return + } + + b := bytes.Buffer{} + + if r.Method != http.MethodGet || r.Header.Get("Upgrade") != "websocket" { + b.WriteString("HTTP/1.1 503 Service Unavailable\r\n") + b.WriteString("Content-Length: 0\r\n") + b.WriteString("Date: " + time.Now().Format(time.RFC1123) + "\r\n") + b.WriteString("\r\n") + + /* + if Debug { + log.Logf("[ohttp] %s <- %s\n%s", c.RemoteAddr(), c.LocalAddr(), b.String()) + } + */ + + b.WriteTo(c.Conn) + return errors.New("bad request") + } + + b.WriteString("HTTP/1.1 101 Switching Protocols\r\n") + b.WriteString("Server: nginx/1.10.0\r\n") + b.WriteString("Date: " + time.Now().Format(time.RFC1123) + "\r\n") + b.WriteString("Connection: Upgrade\r\n") + b.WriteString("Upgrade: websocket\r\n") + b.WriteString(fmt.Sprintf("Sec-WebSocket-Accept: %s\r\n", computeAcceptKey(r.Header.Get("Sec-WebSocket-Key")))) + b.WriteString("\r\n") + + /* + if Debug { + log.Logf("[ohttp] %s <- %s\n%s", c.RemoteAddr(), c.LocalAddr(), b.String()) + } + */ + + if c.rbuf.Len() > 0 { + c.wbuf = b // cache the response header if there are extra data in the request body. + return + } + + _, err = b.WriteTo(c.Conn) + return +} + +func (c *conn) Read(b []byte) (n int, err error) { + if err = c.Handshake(); err != nil { + return + } + + if c.rbuf.Len() > 0 { + return c.rbuf.Read(b) + } + return c.Conn.Read(b) +} + +func (c *conn) Write(b []byte) (n int, err error) { + if err = c.Handshake(); err != nil { + return + } + if c.wbuf.Len() > 0 { + c.wbuf.Write(b) // append the data to the cached header + _, err = c.wbuf.WriteTo(c.Conn) + n = len(b) // exclude the header length + return + } + return c.Conn.Write(b) +} + +var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") + +func computeAcceptKey(challengeKey string) string { + h := sha1.New() + h.Write([]byte(challengeKey)) + h.Write(keyGUID) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} diff --git a/server/listener/obfs/http/listener.go b/server/listener/obfs/http/listener.go new file mode 100644 index 0000000..c8c5f54 --- /dev/null +++ b/server/listener/obfs/http/listener.go @@ -0,0 +1,88 @@ +package http + +import ( + "errors" + "net" + "strconv" + "time" + + "github.com/go-gost/gost/logger" + "github.com/go-gost/gost/server/listener" + "github.com/go-gost/gost/utils" +) + +var ( + _ listener.Listener = (*Listener)(nil) +) + +type Listener struct { + md metadata + net.Listener + logger logger.Logger +} + +func NewListener(opts ...listener.Option) *Listener { + options := &listener.Options{} + for _, opt := range opts { + opt(options) + } + return &Listener{ + logger: options.Logger, + } +} + +func (l *Listener) Init(md listener.Metadata) (err error) { + l.md, err = l.parseMetadata(md) + if err != nil { + return + } + + laddr, err := net.ResolveTCPAddr("tcp", l.md.addr) + if err != nil { + return + } + ln, err := net.ListenTCP("tcp", laddr) + if err != nil { + return + } + + if l.md.keepAlive { + l.Listener = &utils.TCPKeepAliveListener{ + TCPListener: ln, + KeepAlivePeriod: l.md.keepAlivePeriod, + } + return + } + + l.Listener = ln + return +} + +func (l *Listener) Accept() (net.Conn, error) { + c, err := l.Listener.Accept() + if err != nil { + return nil, err + } + + return &conn{Conn: c}, nil +} + +func (l *Listener) parseMetadata(md listener.Metadata) (m metadata, err error) { + if val, ok := md[addr]; ok { + m.addr = val + } else { + err = errors.New("missing address") + return + } + + m.keepAlive = true + if val, ok := md[keepAlive]; ok { + m.keepAlive, _ = strconv.ParseBool(val) + } + + if val, ok := md[keepAlivePeriod]; ok { + m.keepAlivePeriod, _ = time.ParseDuration(val) + } + + return +} diff --git a/server/listener/obfs/http/metadata.go b/server/listener/obfs/http/metadata.go new file mode 100644 index 0000000..a96b8dd --- /dev/null +++ b/server/listener/obfs/http/metadata.go @@ -0,0 +1,19 @@ +package http + +import "time" + +const ( + addr = "addr" + keepAlive = "keepAlive" + keepAlivePeriod = "keepAlivePeriod" +) + +const ( + defaultKeepAlivePeriod = 180 * time.Second +) + +type metadata struct { + addr string + keepAlive bool + keepAlivePeriod time.Duration +} diff --git a/server/listener/obfs/tls/conn.go b/server/listener/obfs/tls/conn.go new file mode 100644 index 0000000..ae6c529 --- /dev/null +++ b/server/listener/obfs/tls/conn.go @@ -0,0 +1,306 @@ +package tls + +import ( + "bytes" + "crypto/rand" + "crypto/tls" + "errors" + "net" + "sync" + "time" + + dissector "github.com/ginuerzh/tls-dissector" +) + +const ( + maxTLSDataLen = 16384 +) + +var ( + cipherSuites = []uint16{ + 0xc02c, 0xc030, 0x009f, 0xcca9, 0xcca8, 0xccaa, 0xc02b, 0xc02f, + 0x009e, 0xc024, 0xc028, 0x006b, 0xc023, 0xc027, 0x0067, 0xc00a, + 0xc014, 0x0039, 0xc009, 0xc013, 0x0033, 0x009d, 0x009c, 0x003d, + 0x003c, 0x0035, 0x002f, 0x00ff, + } + + compressionMethods = []uint8{0x00} + + algorithms = []uint16{ + 0x0601, 0x0602, 0x0603, 0x0501, 0x0502, 0x0503, 0x0401, 0x0402, + 0x0403, 0x0301, 0x0302, 0x0303, 0x0201, 0x0202, 0x0203, + } + + tlsRecordTypes = []uint8{0x16, 0x14, 0x16, 0x17} + tlsVersionMinors = []uint8{0x01, 0x03, 0x03, 0x03} + + ErrBadType = errors.New("bad type") + ErrBadMajorVersion = errors.New("bad major version") + ErrBadMinorVersion = errors.New("bad minor version") + ErrMaxDataLen = errors.New("bad tls data len") +) + +const ( + tlsRecordStateType = iota + tlsRecordStateVersion0 + tlsRecordStateVersion1 + tlsRecordStateLength0 + tlsRecordStateLength1 + tlsRecordStateData +) + +type obfsTLSParser struct { + step uint8 + state uint8 + length uint16 +} + +func (r *obfsTLSParser) Parse(b []byte) (int, error) { + i := 0 + last := 0 + length := len(b) + + for i < length { + ch := b[i] + switch r.state { + case tlsRecordStateType: + if tlsRecordTypes[r.step] != ch { + return 0, ErrBadType + } + r.state = tlsRecordStateVersion0 + i++ + case tlsRecordStateVersion0: + if ch != 0x03 { + return 0, ErrBadMajorVersion + } + r.state = tlsRecordStateVersion1 + i++ + case tlsRecordStateVersion1: + if ch != tlsVersionMinors[r.step] { + return 0, ErrBadMinorVersion + } + r.state = tlsRecordStateLength0 + i++ + case tlsRecordStateLength0: + r.length = uint16(ch) << 8 + r.state = tlsRecordStateLength1 + i++ + case tlsRecordStateLength1: + r.length |= uint16(ch) + if r.step == 0 { + r.length = 91 + } else if r.step == 1 { + r.length = 1 + } else if r.length > maxTLSDataLen { + return 0, ErrMaxDataLen + } + if r.length > 0 { + r.state = tlsRecordStateData + } else { + r.state = tlsRecordStateType + r.step++ + } + i++ + case tlsRecordStateData: + left := uint16(length - i) + if left > r.length { + left = r.length + } + if r.step >= 2 { + skip := i - last + copy(b[last:], b[i:length]) + length -= int(skip) + last += int(left) + i = last + } else { + i += int(left) + } + r.length -= left + if r.length == 0 { + if r.step < 3 { + r.step++ + } + r.state = tlsRecordStateType + } + } + } + + if last == 0 { + return 0, nil + } else if last < length { + length -= last + } + + return length, nil +} + +type conn struct { + net.Conn + rbuf bytes.Buffer + wbuf bytes.Buffer + host string + handshaked chan struct{} + parser *obfsTLSParser + handshakeMutex sync.Mutex +} + +// newConn creates a connection for obfs-tls server. +func newConn(c net.Conn, host string) net.Conn { + return &conn{ + Conn: c, + host: host, + handshaked: make(chan struct{}), + } +} + +func (c *conn) Handshaked() bool { + select { + case <-c.handshaked: + return true + default: + return false + } +} + +func (c *conn) Handshake(payload []byte) (err error) { + c.handshakeMutex.Lock() + defer c.handshakeMutex.Unlock() + + if c.Handshaked() { + return + } + + if err = c.handshake(); err != nil { + return + } + + close(c.handshaked) + return nil +} + +func (c *conn) handshake() error { + record := &dissector.Record{} + if _, err := record.ReadFrom(c.Conn); err != nil { + // log.Log(err) + return err + } + if record.Type != dissector.Handshake { + return dissector.ErrBadType + } + + clientMsg := &dissector.ClientHelloMsg{} + if err := clientMsg.Decode(record.Opaque); err != nil { + // log.Log(err) + return err + } + + for _, ext := range clientMsg.Extensions { + if ext.Type() == dissector.ExtSessionTicket { + b, err := ext.Encode() + if err != nil { + // log.Log(err) + return err + } + c.rbuf.Write(b) + break + } + } + + serverMsg := &dissector.ServerHelloMsg{ + Version: tls.VersionTLS12, + SessionID: clientMsg.SessionID, + CipherSuite: 0xcca8, + CompressionMethod: 0x00, + Extensions: []dissector.Extension{ + &dissector.RenegotiationInfoExtension{}, + &dissector.ExtendedMasterSecretExtension{}, + &dissector.ECPointFormatsExtension{ + Formats: []uint8{0x00}, + }, + }, + } + + serverMsg.Random.Time = uint32(time.Now().Unix()) + rand.Read(serverMsg.Random.Opaque[:]) + b, err := serverMsg.Encode() + if err != nil { + return err + } + + record = &dissector.Record{ + Type: dissector.Handshake, + Version: tls.VersionTLS10, + Opaque: b, + } + + if _, err := record.WriteTo(&c.wbuf); err != nil { + return err + } + + record = &dissector.Record{ + Type: dissector.ChangeCipherSpec, + Version: tls.VersionTLS12, + Opaque: []byte{0x01}, + } + if _, err := record.WriteTo(&c.wbuf); err != nil { + return err + } + return nil +} + +func (c *conn) Read(b []byte) (n int, err error) { + if err = c.Handshake(nil); err != nil { + return + } + + select { + case <-c.handshaked: + } + + if c.rbuf.Len() > 0 { + return c.rbuf.Read(b) + } + record := &dissector.Record{} + if _, err = record.ReadFrom(c.Conn); err != nil { + return + } + n = copy(b, record.Opaque) + _, err = c.rbuf.Write(record.Opaque[n:]) + return +} + +func (c *conn) Write(b []byte) (n int, err error) { + n = len(b) + if !c.Handshaked() { + if err = c.Handshake(b); err != nil { + return + } + } + + for len(b) > 0 { + data := b + if len(b) > maxTLSDataLen { + data = b[:maxTLSDataLen] + b = b[maxTLSDataLen:] + } else { + b = b[:0] + } + record := &dissector.Record{ + Type: dissector.AppData, + Version: tls.VersionTLS12, + Opaque: data, + } + + if c.wbuf.Len() > 0 { + record.Type = dissector.Handshake + record.WriteTo(&c.wbuf) + _, err = c.wbuf.WriteTo(c.Conn) + return + } + + if _, err = record.WriteTo(c.Conn); err != nil { + return + } + } + return +} diff --git a/server/listener/obfs/tls/listener.go b/server/listener/obfs/tls/listener.go new file mode 100644 index 0000000..92e34b3 --- /dev/null +++ b/server/listener/obfs/tls/listener.go @@ -0,0 +1,88 @@ +package tls + +import ( + "errors" + "net" + "strconv" + "time" + + "github.com/go-gost/gost/logger" + "github.com/go-gost/gost/server/listener" + "github.com/go-gost/gost/utils" +) + +var ( + _ listener.Listener = (*Listener)(nil) +) + +type Listener struct { + md metadata + net.Listener + logger logger.Logger +} + +func NewListener(opts ...listener.Option) *Listener { + options := &listener.Options{} + for _, opt := range opts { + opt(options) + } + return &Listener{ + logger: options.Logger, + } +} + +func (l *Listener) Init(md listener.Metadata) (err error) { + l.md, err = l.parseMetadata(md) + if err != nil { + return + } + + laddr, err := net.ResolveTCPAddr("tcp", l.md.addr) + if err != nil { + return + } + ln, err := net.ListenTCP("tcp", laddr) + if err != nil { + return + } + + if l.md.keepAlive { + l.Listener = &utils.TCPKeepAliveListener{ + TCPListener: ln, + KeepAlivePeriod: l.md.keepAlivePeriod, + } + return + } + + l.Listener = ln + return +} + +func (l *Listener) Accept() (net.Conn, error) { + c, err := l.Listener.Accept() + if err != nil { + return nil, err + } + + return &conn{Conn: c}, nil +} + +func (l *Listener) parseMetadata(md listener.Metadata) (m metadata, err error) { + if val, ok := md[addr]; ok { + m.addr = val + } else { + err = errors.New("missing address") + return + } + + m.keepAlive = true + if val, ok := md[keepAlive]; ok { + m.keepAlive, _ = strconv.ParseBool(val) + } + + if val, ok := md[keepAlivePeriod]; ok { + m.keepAlivePeriod, _ = time.ParseDuration(val) + } + + return +} diff --git a/server/listener/obfs/tls/metadata.go b/server/listener/obfs/tls/metadata.go new file mode 100644 index 0000000..5e01136 --- /dev/null +++ b/server/listener/obfs/tls/metadata.go @@ -0,0 +1,19 @@ +package tls + +import "time" + +const ( + addr = "addr" + keepAlive = "keepAlive" + keepAlivePeriod = "keepAlivePeriod" +) + +const ( + defaultKeepAlivePeriod = 180 * time.Second +) + +type metadata struct { + addr string + keepAlive bool + keepAlivePeriod time.Duration +}