diff --git a/common/protocol/bittorrent/bittorrent.go b/common/protocol/bittorrent/bittorrent.go new file mode 100644 index 00000000..fbb81570 --- /dev/null +++ b/common/protocol/bittorrent/bittorrent.go @@ -0,0 +1,32 @@ +package bittorrent + +import ( + "errors" + + "v2ray.com/core" +) + +type SniffHeader struct { +} + +func (h *SniffHeader) Protocol() string { + return "bittorrent" +} + +func (h *SniffHeader) Domain() string { + return "" +} + +var errNotBittorrent = errors.New("not bittorrent header") + +func SniffBittorrent(b []byte) (*SniffHeader, error) { + if len(b) < 20 { + return nil, core.ErrNoClue + } + + if b[0] == 19 && string(b[1:20]) == "BitTorrent protocol" { + return &SniffHeader{}, nil + } + + return nil, errNotBittorrent +} diff --git a/common/protocol/http/sniff.go b/common/protocol/http/sniff.go new file mode 100644 index 00000000..db478da6 --- /dev/null +++ b/common/protocol/http/sniff.go @@ -0,0 +1,90 @@ +package http + +import ( + "bytes" + "errors" + "strings" + + "v2ray.com/core" +) + +type version byte + +const ( + HTTP1 version = iota + HTTP2 +) + +type SniffHeader struct { + version version + host string +} + +func (h *SniffHeader) Protocol() string { + switch h.version { + case HTTP1: + return "http1" + case HTTP2: + return "http2" + default: + return "unknown" + } +} + +func (h *SniffHeader) Domain() string { + return h.host +} + +var ( + methods = [...]string{"get", "post", "head", "put", "delete", "options", "connect"} + + errNotHTTPMethod = errors.New("not an HTTP method") +) + +func beginWithHTTPMethod(b []byte) error { + for _, m := range methods { + if len(b) >= len(m) && strings.ToLower(string(b[:len(m)])) == m { + return nil + } + + if len(b) < len(m) { + return core.ErrNoClue + } + } + + return errNotHTTPMethod +} + +func SniffHTTP(b []byte) (*SniffHeader, error) { + if err := beginWithHTTPMethod(b); err != nil { + return nil, err + } + + sh := &SniffHeader{ + version: HTTP1, + } + + headers := bytes.Split(b, []byte{'\n'}) + for i := 1; i < len(headers); i++ { + header := headers[i] + if len(header) == 0 { + return nil, core.ErrNoClue + } + parts := bytes.SplitN(header, []byte{':'}, 2) + if len(parts) != 2 { + continue + } + key := strings.ToLower(string(parts[0])) + value := strings.ToLower(string(bytes.Trim(parts[1], " "))) + if key == "host" { + domain := strings.Split(value, ":") + sh.host = strings.TrimSpace(domain[0]) + } + } + + if len(sh.host) > 0 { + return sh, nil + } + + return nil, core.ErrNoClue +} diff --git a/common/protocol/http/sniff_test.go b/common/protocol/http/sniff_test.go new file mode 100644 index 00000000..2499b05a --- /dev/null +++ b/common/protocol/http/sniff_test.go @@ -0,0 +1,106 @@ +package http_test + +import ( + "testing" + + "v2ray.com/core/common/compare" + . "v2ray.com/core/common/protocol/http" +) + +func TestHTTPHeaders(t *testing.T) { + cases := []struct { + input string + domain string + err bool + }{ + { + input: `GET /tutorials/other/top-20-mysql-best-practices/ HTTP/1.1 +Host: net.tutsplus.com +User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729) +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 300 +Connection: keep-alive +Cookie: PHPSESSID=r2t5uvjq435r4q7ib3vtdjq120 +Pragma: no-cache +Cache-Control: no-cache`, + domain: "net.tutsplus.com", + }, + { + input: `POST /foo.php HTTP/1.1 +Host: localhost +User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729) +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 300 +Connection: keep-alive +Referer: http://localhost/test.php +Content-Type: application/x-www-form-urlencoded +Content-Length: 43 + +first_name=John&last_name=Doe&action=Submit`, + domain: "localhost", + }, + { + input: `X /foo.php HTTP/1.1 +Host: localhost +User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729) +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 300 +Connection: keep-alive +Referer: http://localhost/test.php +Content-Type: application/x-www-form-urlencoded +Content-Length: 43 + +first_name=John&last_name=Doe&action=Submit`, + domain: "", + err: true, + }, + { + input: `GET /foo.php HTTP/1.1 +User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729) +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 +Accept-Language: en-us,en;q=0.5 +Accept-Encoding: gzip,deflate +Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive: 300 +Connection: keep-alive +Referer: http://localhost/test.php +Content-Type: application/x-www-form-urlencoded +Content-Length: 43 + +Host: localhost +first_name=John&last_name=Doe&action=Submit`, + domain: "", + err: true, + }, + { + input: `GET /tutorials/other/top-20-mysql-best-practices/ HTTP/1.1`, + domain: "", + err: true, + }, + } + + for _, test := range cases { + header, err := SniffHTTP([]byte(test.input)) + if test.err { + if err == nil { + t.Errorf("Expect error but nil, in test: %v", test) + } + } else { + if err != nil { + t.Errorf("Expect no error but actually %s in test %v", err.Error(), test) + } + if err := compare.StringEqualWithDetail(header.Domain(), test.domain); err != nil { + t.Error(err) + } + } + } +} diff --git a/common/protocol/tls/sniff.go b/common/protocol/tls/sniff.go new file mode 100644 index 00000000..36ee1792 --- /dev/null +++ b/common/protocol/tls/sniff.go @@ -0,0 +1,147 @@ +package tls + +import ( + "errors" + "strings" + + "v2ray.com/core" + "v2ray.com/core/common/serial" +) + +type SniffHeader struct { + domain string +} + +func (h *SniffHeader) Protocol() string { + return "tls" +} + +func (h *SniffHeader) Domain() string { + return h.domain +} + +var errNotTLS = errors.New("not TLS header") +var errNotClientHello = errors.New("not client hello") + +func IsValidTLSVersion(major, minor byte) bool { + return major == 3 +} + +// ReadClientHello returns server name (if any) from TLS client hello message. +// https://github.com/golang/go/blob/master/src/crypto/tls/handshake_messages.go#L300 +func ReadClientHello(data []byte, h *SniffHeader) error { + if len(data) < 42 { + return core.ErrNoClue + } + sessionIDLen := int(data[38]) + if sessionIDLen > 32 || len(data) < 39+sessionIDLen { + return core.ErrNoClue + } + data = data[39+sessionIDLen:] + if len(data) < 2 { + return core.ErrNoClue + } + // cipherSuiteLen is the number of bytes of cipher suite numbers. Since + // they are uint16s, the number must be even. + cipherSuiteLen := int(data[0])<<8 | int(data[1]) + if cipherSuiteLen%2 == 1 || len(data) < 2+cipherSuiteLen { + return errNotClientHello + } + data = data[2+cipherSuiteLen:] + if len(data) < 1 { + return core.ErrNoClue + } + compressionMethodsLen := int(data[0]) + if len(data) < 1+compressionMethodsLen { + return core.ErrNoClue + } + data = data[1+compressionMethodsLen:] + + if len(data) == 0 { + return errNotClientHello + } + if len(data) < 2 { + return errNotClientHello + } + + extensionsLength := int(data[0])<<8 | int(data[1]) + data = data[2:] + if extensionsLength != len(data) { + return errNotClientHello + } + + for len(data) != 0 { + if len(data) < 4 { + return errNotClientHello + } + extension := uint16(data[0])<<8 | uint16(data[1]) + length := int(data[2])<<8 | int(data[3]) + data = data[4:] + if len(data) < length { + return errNotClientHello + } + + switch extension { + case 0x00: /* extensionServerName */ + d := data[:length] + if len(d) < 2 { + return errNotClientHello + } + namesLen := int(d[0])<<8 | int(d[1]) + d = d[2:] + if len(d) != namesLen { + return errNotClientHello + } + for len(d) > 0 { + if len(d) < 3 { + return errNotClientHello + } + nameType := d[0] + nameLen := int(d[1])<<8 | int(d[2]) + d = d[3:] + if len(d) < nameLen { + return errNotClientHello + } + if nameType == 0 { + serverName := string(d[:nameLen]) + // An SNI value may not include a + // trailing dot. See + // https://tools.ietf.org/html/rfc6066#section-3. + if strings.HasSuffix(serverName, ".") { + return errNotClientHello + } + h.domain = serverName + return nil + } + d = d[nameLen:] + } + } + data = data[length:] + } + + return errNotTLS +} + +func SniffTLS(b []byte) (*SniffHeader, error) { + if len(b) < 5 { + return nil, core.ErrNoClue + } + + if b[0] != 0x16 /* TLS Handshake */ { + return nil, errNotTLS + } + if !IsValidTLSVersion(b[1], b[2]) { + return nil, errNotTLS + } + headerLen := int(serial.BytesToUint16(b[3:5])) + if 5+headerLen > len(b) { + return nil, core.ErrNoClue + } + + h := &SniffHeader{} + err := ReadClientHello(b[5:5+headerLen], h) + if err == nil { + return h, nil + } + return nil, err +} diff --git a/common/protocol/tls/sniff_test.go b/common/protocol/tls/sniff_test.go new file mode 100644 index 00000000..d3fe6262 --- /dev/null +++ b/common/protocol/tls/sniff_test.go @@ -0,0 +1,102 @@ +package tls_test + +import ( + "testing" + + "v2ray.com/core/common/compare" + . "v2ray.com/core/common/protocol/tls" +) + +func TestTLSHeaders(t *testing.T) { + cases := []struct { + input []byte + domain string + err bool + }{ + { + input: []byte{ + 0x16, 0x03, 0x01, 0x00, 0xc8, 0x01, 0x00, 0x00, + 0xc4, 0x03, 0x03, 0x1a, 0xac, 0xb2, 0xa8, 0xfe, + 0xb4, 0x96, 0x04, 0x5b, 0xca, 0xf7, 0xc1, 0xf4, + 0x2e, 0x53, 0x24, 0x6e, 0x34, 0x0c, 0x58, 0x36, + 0x71, 0x97, 0x59, 0xe9, 0x41, 0x66, 0xe2, 0x43, + 0xa0, 0x13, 0xb6, 0x00, 0x00, 0x20, 0x1a, 0x1a, + 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, + 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0x14, 0xcc, 0x13, + 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, + 0x00, 0x2f, 0x00, 0x35, 0x00, 0x0a, 0x01, 0x00, + 0x00, 0x7b, 0xba, 0xba, 0x00, 0x00, 0xff, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, + 0x14, 0x00, 0x00, 0x11, 0x63, 0x2e, 0x73, 0x2d, + 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, + 0x74, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x17, 0x00, + 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x0d, 0x00, + 0x14, 0x00, 0x12, 0x04, 0x03, 0x08, 0x04, 0x04, + 0x01, 0x05, 0x03, 0x08, 0x05, 0x05, 0x01, 0x08, + 0x06, 0x06, 0x01, 0x02, 0x01, 0x00, 0x05, 0x00, + 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, + 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c, + 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, + 0x2f, 0x31, 0x2e, 0x31, 0x00, 0x0b, 0x00, 0x02, + 0x01, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, + 0xaa, 0xaa, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, + 0xaa, 0xaa, 0x00, 0x01, 0x00, + }, + domain: "c.s-microsoft.com", + err: false, + }, + { + input: []byte{ + 0x16, 0x03, 0x01, 0x00, 0xee, 0x01, 0x00, 0x00, + 0xea, 0x03, 0x03, 0xe7, 0x91, 0x9e, 0x93, 0xca, + 0x78, 0x1b, 0x3c, 0xe0, 0x65, 0x25, 0x58, 0xb5, + 0x93, 0xe1, 0x0f, 0x85, 0xec, 0x9a, 0x66, 0x8e, + 0x61, 0x82, 0x88, 0xc8, 0xfc, 0xae, 0x1e, 0xca, + 0xd7, 0xa5, 0x63, 0x20, 0xbd, 0x1c, 0x00, 0x00, + 0x8b, 0xee, 0x09, 0xe3, 0x47, 0x6a, 0x0e, 0x74, + 0xb0, 0xbc, 0xa3, 0x02, 0xa7, 0x35, 0xe8, 0x85, + 0x70, 0x7c, 0x7a, 0xf0, 0x00, 0xdf, 0x4a, 0xea, + 0x87, 0x01, 0x14, 0x91, 0x00, 0x20, 0xea, 0xea, + 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, + 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0x14, 0xcc, 0x13, + 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, + 0x00, 0x2f, 0x00, 0x35, 0x00, 0x0a, 0x01, 0x00, + 0x00, 0x81, 0x9a, 0x9a, 0x00, 0x00, 0xff, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, + 0x16, 0x00, 0x00, 0x13, 0x77, 0x77, 0x77, 0x30, + 0x37, 0x2e, 0x63, 0x6c, 0x69, 0x63, 0x6b, 0x74, + 0x61, 0x6c, 0x65, 0x2e, 0x6e, 0x65, 0x74, 0x00, + 0x17, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, + 0x0d, 0x00, 0x14, 0x00, 0x12, 0x04, 0x03, 0x08, + 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05, + 0x01, 0x08, 0x06, 0x06, 0x01, 0x02, 0x01, 0x00, + 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x12, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, + 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, + 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x75, 0x50, + 0x00, 0x00, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, + 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x9a, 0x9a, + 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x8a, 0x8a, + 0x00, 0x01, 0x00, + }, + domain: "www07.clicktale.net", + err: false, + }, + } + + for _, test := range cases { + header, err := SniffTLS(test.input) + if test.err { + if err == nil { + t.Errorf("Exepct error but nil in test %v", test) + } + } else { + if err != nil { + t.Errorf("Expect no error but actually %s in test %v", err.Error(), test) + } + if err := compare.StringEqualWithDetail(header.Domain(), test.domain); err != nil { + t.Error(err) + } + } + } +}