diff --git a/adapter/outbound/snell.go b/adapter/outbound/snell.go index 92343055..7b272d5d 100644 --- a/adapter/outbound/snell.go +++ b/adapter/outbound/snell.go @@ -99,7 +99,13 @@ func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o tcpKeepAlive(c) c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption}) - return s.ListenPacketOnStreamConn(c, metadata) + err = snell.WriteUDPHeader(c, s.version) + if err != nil { + return nil, err + } + + pc := snell.PacketConn(c) + return newPacketConn(pc, s), nil } // ListenPacketOnStreamConn implements C.ProxyAdapter diff --git a/adapter/outbound/trojan.go b/adapter/outbound/trojan.go index 46586673..31811c51 100644 --- a/adapter/outbound/trojan.go +++ b/adapter/outbound/trojan.go @@ -161,7 +161,13 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata, } } - return t.ListenPacketOnStreamConn(c, metadata) + err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata)) + if err != nil { + return nil, err + } + + pc := t.instance.PacketConn(c) + return newPacketConn(pc, t), err } // ListenPacketOnStreamConn implements C.ProxyAdapter diff --git a/component/sniffer/dispatcher.go b/component/sniffer/dispatcher.go index 9d47d36f..5ca44f06 100644 --- a/component/sniffer/dispatcher.go +++ b/component/sniffer/dispatcher.go @@ -19,6 +19,7 @@ import ( var ( ErrorUnsupportedSniffer = errors.New("unsupported sniffer") ErrorSniffFailed = errors.New("all sniffer failed") + ErrNoClue = errors.New("not enough information for making a decision") ) var Dispatcher SnifferDispatcher diff --git a/component/sniffer/http_sniffer.go b/component/sniffer/http_sniffer.go new file mode 100644 index 00000000..84ab4103 --- /dev/null +++ b/component/sniffer/http_sniffer.go @@ -0,0 +1,109 @@ +package sniffer + +import ( + "bytes" + "errors" + C "github.com/Dreamacro/clash/constant" + "net" + "strconv" + "strings" +) + +var ( + // refer to https://pkg.go.dev/net/http@master#pkg-constants + methods = [...]string{"get", "post", "head", "put", "delete", "options", "connect", "patch", "trace"} + errNotHTTPMethod = errors.New("not an HTTP method") +) + +type version byte + +const ( + HTTP1 version = iota + HTTP2 +) + +type HTTPSniffer struct { + version version + host string +} + +func (http *HTTPSniffer) Protocol() string { + switch http.version { + case HTTP1: + return "http1" + case HTTP2: + return "http2" + default: + return "unknown" + } +} + +func (http *HTTPSniffer) SupportNetwork() C.NetWork { + return C.TCP +} + +func (http *HTTPSniffer) SniffTCP(bytes []byte) (string, error) { + domain, err := SniffHTTP(bytes) + if err == nil { + return *domain, nil + } else { + return "", err + } +} + +func beginWithHTTPMethod(b []byte) error { + for _, m := range &methods { + if len(b) >= len(m) && strings.EqualFold(string(b[:len(m)]), m) { + return nil + } + + if len(b) < len(m) { + return ErrNoClue + } + } + return errNotHTTPMethod +} + +func SniffHTTP(b []byte) (*string, error) { + if err := beginWithHTTPMethod(b); err != nil { + return nil, err + } + + _ = &HTTPSniffer{ + version: HTTP1, + } + + headers := bytes.Split(b, []byte{'\n'}) + for i := 1; i < len(headers); i++ { + header := headers[i] + if len(header) == 0 { + break + } + parts := bytes.SplitN(header, []byte{':'}, 2) + if len(parts) != 2 { + continue + } + key := strings.ToLower(string(parts[0])) + if key == "host" { + port := 80 + rawHost := strings.ToLower(string(bytes.TrimSpace(parts[1]))) + host, rawPort, err := net.SplitHostPort(rawHost) + if err != nil { + if addrError, ok := err.(*net.AddrError); ok && strings.Contains(addrError.Err, "missing port") { + host = rawHost + } else { + return nil, err + } + } else if len(rawPort) > 0 { + intPort, err := strconv.ParseUint(rawPort, 0, 16) + if err != nil { + return nil, err + } + port = int(intPort) + } + host = net.JoinHostPort(host, strconv.Itoa(port)) + return &host, nil + } + } + return nil, ErrNoClue +} diff --git a/component/sniffer/quic_sniffer.go b/component/sniffer/quic_sniffer.go new file mode 100644 index 00000000..de78cf82 --- /dev/null +++ b/component/sniffer/quic_sniffer.go @@ -0,0 +1,3 @@ +package sniffer + +//TODO diff --git a/component/sniffer/tls_sniffer.go b/component/sniffer/tls_sniffer.go index 6af0b97b..f5a8fd99 100644 --- a/component/sniffer/tls_sniffer.go +++ b/component/sniffer/tls_sniffer.go @@ -11,7 +11,6 @@ import ( var ( errNotTLS = errors.New("not TLS header") errNotClientHello = errors.New("not client hello") - ErrNoClue = errors.New("not enough information for making a decision") ) type TLSSniffer struct { diff --git a/constant/sniffer.go b/constant/sniffer.go index 5454279a..0592b87a 100644 --- a/constant/sniffer.go +++ b/constant/sniffer.go @@ -8,10 +8,11 @@ type Sniffer interface { const ( TLS SnifferType = iota + HTTP SnifferType ) var ( - SnifferList = []SnifferType{TLS} + SnifferList = []SnifferType{TLS, HTTP} ) type SnifferType int @@ -20,6 +21,8 @@ func (rt SnifferType) String() string { switch rt { case TLS: return "TLS" + case HTTP: + return "HTTP" default: return "Unknown" } diff --git a/dns/doq.go b/dns/doq.go index a0682745..8764ebab 100644 --- a/dns/doq.go +++ b/dns/doq.go @@ -138,6 +138,8 @@ func (dc *quicClient) openSession() (quic.Connection, error) { quicConfig := &quic.Config{ ConnectionIDLength: 12, HandshakeIdleTimeout: time.Second * 8, + MaxIncomingStreams: 4, + MaxIdleTimeout: time.Second * 45, } log.Debugln("opening session to %s", dc.addr)