diff --git a/common/snifer/tls/sniff.go b/common/snifer/tls/sniff.go new file mode 100644 index 00000000..1471fc68 --- /dev/null +++ b/common/snifer/tls/sniff.go @@ -0,0 +1,148 @@ +package tls + +import ( + "encoding/binary" + "errors" + "strings" +) + +var ErrNoClue = errors.New("not enough information for making a decision") + +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") + 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 ErrNoClue + } + sessionIDLen := int(data[38]) + if sessionIDLen > 32 || len(data) < 39+sessionIDLen { + return ErrNoClue + } + data = data[39+sessionIDLen:] + if len(data) < 2 { + return 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 ErrNoClue + } + compressionMethodsLen := int(data[0]) + if len(data) < 1+compressionMethodsLen { + return 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 + } + + if extension == 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, ErrNoClue + } + + if b[0] != 0x16 /* TLS Handshake */ { + return nil, errNotTLS + } + if !IsValidTLSVersion(b[1], b[2]) { + return nil, errNotTLS + } + headerLen := int(binary.BigEndian.Uint16(b[3:5])) + if 5+headerLen > len(b) { + return nil, ErrNoClue + } + + h := &SniffHeader{} + err := ReadClientHello(b[5:5+headerLen], h) + if err == nil { + return h, nil + } + return nil, err +} diff --git a/common/snifer/tls/sniff_test.go b/common/snifer/tls/sniff_test.go new file mode 100644 index 00000000..26f5f1ee --- /dev/null +++ b/common/snifer/tls/sniff_test.go @@ -0,0 +1,159 @@ +package tls + +import ( + "testing" +) + +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, + }, + { + input: []byte{ + 0x16, 0x03, 0x01, 0x00, 0xe6, 0x01, 0x00, 0x00, 0xe2, 0x03, 0x03, 0x81, 0x47, 0xc1, + 0x66, 0xd5, 0x1b, 0xfa, 0x4b, 0xb5, 0xe0, 0x2a, 0xe1, 0xa7, 0x87, 0x13, 0x1d, 0x11, 0xaa, 0xc6, + 0xce, 0xfc, 0x7f, 0xab, 0x94, 0xc8, 0x62, 0xad, 0xc8, 0xab, 0x0c, 0xdd, 0xcb, 0x20, 0x6f, 0x9d, + 0x07, 0xf1, 0x95, 0x3e, 0x99, 0xd8, 0xf3, 0x6d, 0x97, 0xee, 0x19, 0x0b, 0x06, 0x1b, 0xf4, 0x84, + 0x0b, 0xb6, 0x8f, 0xcc, 0xde, 0xe2, 0xd0, 0x2d, 0x6b, 0x0c, 0x1f, 0x52, 0x53, 0x13, 0x00, 0x08, + 0x13, 0x02, 0x13, 0x03, 0x13, 0x01, 0x00, 0xff, 0x01, 0x00, 0x00, 0x91, 0x00, 0x00, 0x00, 0x0c, + 0x00, 0x0a, 0x00, 0x00, 0x07, 0x64, 0x6f, 0x67, 0x66, 0x69, 0x73, 0x68, 0x00, 0x0b, 0x00, 0x04, + 0x03, 0x00, 0x01, 0x02, 0x00, 0x0a, 0x00, 0x0c, 0x00, 0x0a, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x1e, + 0x00, 0x19, 0x00, 0x18, 0x00, 0x23, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, + 0x00, 0x0d, 0x00, 0x1e, 0x00, 0x1c, 0x04, 0x03, 0x05, 0x03, 0x06, 0x03, 0x08, 0x07, 0x08, 0x08, + 0x08, 0x09, 0x08, 0x0a, 0x08, 0x0b, 0x08, 0x04, 0x08, 0x05, 0x08, 0x06, 0x04, 0x01, 0x05, 0x01, + 0x06, 0x01, 0x00, 0x2b, 0x00, 0x07, 0x06, 0x7f, 0x1c, 0x7f, 0x1b, 0x7f, 0x1a, 0x00, 0x2d, 0x00, + 0x02, 0x01, 0x01, 0x00, 0x33, 0x00, 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20, 0x2f, 0x35, 0x0c, + 0xb6, 0x90, 0x0a, 0xb7, 0xd5, 0xc4, 0x1b, 0x2f, 0x60, 0xaa, 0x56, 0x7b, 0x3f, 0x71, 0xc8, 0x01, + 0x7e, 0x86, 0xd3, 0xb7, 0x0c, 0x29, 0x1a, 0x9e, 0x5b, 0x38, 0x3f, 0x01, 0x72, + }, + domain: "dogfish", + err: false, + }, + { + input: []byte{ + 0x16, 0x03, 0x01, 0x01, 0x03, 0x01, 0x00, 0x00, + 0xff, 0x03, 0x03, 0x3d, 0x89, 0x52, 0x9e, 0xee, + 0xbe, 0x17, 0x63, 0x75, 0xef, 0x29, 0xbd, 0x14, + 0x6a, 0x49, 0xe0, 0x2c, 0x37, 0x57, 0x71, 0x62, + 0x82, 0x44, 0x94, 0x8f, 0x6e, 0x94, 0x08, 0x45, + 0x7f, 0xdb, 0xc1, 0x00, 0x00, 0x3e, 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, 0x13, 0x02, 0x13, 0x03, + 0x13, 0x01, 0x00, 0x3d, 0x00, 0x3c, 0x00, 0x35, + 0x00, 0x2f, 0x00, 0xff, 0x01, 0x00, 0x00, 0x98, + 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x00, + 0x0b, 0x31, 0x30, 0x2e, 0x34, 0x32, 0x2e, 0x30, + 0x2e, 0x32, 0x34, 0x33, 0x00, 0x0b, 0x00, 0x04, + 0x03, 0x00, 0x01, 0x02, 0x00, 0x0a, 0x00, 0x0a, + 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, + 0x00, 0x18, 0x00, 0x23, 0x00, 0x00, 0x00, 0x0d, + 0x00, 0x20, 0x00, 0x1e, 0x04, 0x03, 0x05, 0x03, + 0x06, 0x03, 0x08, 0x04, 0x08, 0x05, 0x08, 0x06, + 0x04, 0x01, 0x05, 0x01, 0x06, 0x01, 0x02, 0x03, + 0x02, 0x01, 0x02, 0x02, 0x04, 0x02, 0x05, 0x02, + 0x06, 0x02, 0x00, 0x16, 0x00, 0x00, 0x00, 0x17, + 0x00, 0x00, 0x00, 0x2b, 0x00, 0x09, 0x08, 0x7f, + 0x14, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, 0x00, + 0x2d, 0x00, 0x03, 0x02, 0x01, 0x00, 0x00, 0x28, + 0x00, 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20, + 0x13, 0x7c, 0x6e, 0x97, 0xc4, 0xfd, 0x09, 0x2e, + 0x70, 0x2f, 0x73, 0x5a, 0x9b, 0x57, 0x4d, 0x5f, + 0x2b, 0x73, 0x2c, 0xa5, 0x4a, 0x98, 0x40, 0x3d, + 0x75, 0x6e, 0xb4, 0x76, 0xf9, 0x48, 0x8f, 0x36, + }, + domain: "10.42.0.243", + 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 header.Domain() != test.domain { + t.Error("expect domain ", test.domain, " but got ", header.Domain()) + } + } + } +} diff --git a/component/resolver/enhancer.go b/component/resolver/enhancer.go index 9df3f54b..77f18374 100644 --- a/component/resolver/enhancer.go +++ b/component/resolver/enhancer.go @@ -14,6 +14,7 @@ type Enhancer interface { IsExistFakeIP(net.IP) bool FindHostByIP(net.IP) (string, bool) FlushFakeIP() error + InsertHostByIP(net.IP, string) } func FakeIPEnabled() bool { @@ -56,6 +57,12 @@ func IsExistFakeIP(ip net.IP) bool { return false } +func InsertHostByIP(ip net.IP, host string) { + if mapper := DefaultHostMapper; mapper != nil { + mapper.InsertHostByIP(ip, host) + } +} + func FindHostByIP(ip net.IP) (string, bool) { if mapper := DefaultHostMapper; mapper != nil { return mapper.FindHostByIP(ip) diff --git a/dns/enhancer.go b/dns/enhancer.go index 9bf568c7..016ff02a 100644 --- a/dns/enhancer.go +++ b/dns/enhancer.go @@ -74,6 +74,12 @@ func (h *ResolverEnhancer) FindHostByIP(ip net.IP) (string, bool) { return "", false } +func (h *ResolverEnhancer) InsertHostByIP(ip net.IP, host string) { + if mapping := h.mapping; mapping != nil { + h.mapping.Set(ip.String(), host) + } +} + func (h *ResolverEnhancer) PatchFrom(o *ResolverEnhancer) { if h.mapping != nil && o.mapping != nil { o.mapping.CloneTo(h.mapping) diff --git a/tunnel/statistic/tracker.go b/tunnel/statistic/tracker.go index 1f5f1f9c..77713da2 100644 --- a/tunnel/statistic/tracker.go +++ b/tunnel/statistic/tracker.go @@ -1,11 +1,15 @@ package statistic import ( + "errors" "net" "time" + "github.com/Dreamacro/clash/common/snifer/tls" + "github.com/Dreamacro/clash/component/resolver" C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" "github.com/gofrs/uuid" "go.uber.org/atomic" ) @@ -48,7 +52,21 @@ func (tt *tcpTracker) Write(b []byte) (int, error) { n, err := tt.Conn.Write(b) upload := int64(n) tt.manager.PushUploaded(upload) + if tt.UploadTotal.Load() < 128 && tt.Metadata.Host == "" && (tt.Metadata.DstPort == "443" || tt.Metadata.DstPort == "8443") { + header, err := tls.SniffTLS(b) + if err != nil { + // log.Errorln("Expect no error but actually %s %s:%s:%s", err.Error(), tt.Metadata.Host, tt.Metadata.DstIP.String(), tt.Metadata.DstPort) + } else { + tt.Metadata.Host = header.Domain() + resolver.InsertHostByIP(tt.Metadata.DstIP, tt.Metadata.Host) + log.Errorln("sni %s %s", tt.Metadata.Host, tt.Metadata.DstIP.String()) + tt.manager.Leave(tt) + tt.Conn.Close() + return n, errors.New("sni update") + } + } tt.UploadTotal.Add(upload) + return n, err }