From f036e06f6f70c790f7d1dc51e3949d13e98efa5a Mon Sep 17 00:00:00 2001 From: yaling888 <73897884+yaling888@users.noreply.github.com> Date: Sun, 10 Apr 2022 03:59:27 +0800 Subject: [PATCH] Feature: MITM rewrite --- README.md | 40 ++- adapter/inbound/mitm.go | 22 ++ adapter/outbound/http.go | 4 + adapter/outbound/mitm.go | 68 ++++ common/cert/cert.go | 282 ++++++++++++++++ common/cert/cert_test.go | 76 +++++ common/cert/storage.go | 32 ++ component/geodata/memconservative/cache.go | 4 +- component/geodata/standard/standard.go | 2 +- config/config.go | 89 ++++- constant/adapters.go | 3 + constant/metadata.go | 4 + constant/path.go | 8 +- constant/rewrite.go | 82 +++++ constant/rule.go | 3 + dns/middleware.go | 15 +- go.mod | 10 +- go.sum | 16 +- hub/executor/executor.go | 18 +- hub/route/configs.go | 2 + listener/http/proxy.go | 20 +- listener/http/utils.go | 8 +- listener/listener.go | 109 +++++++ listener/mitm/client.go | 54 ++++ listener/mitm/proxy.go | 357 +++++++++++++++++++++ listener/mitm/server.go | 90 ++++++ listener/mitm/session.go | 56 ++++ listener/mitm/utils.go | 100 ++++++ rewrite/base.go | 72 +++++ rewrite/handler.go | 202 ++++++++++++ rewrite/parser.go | 78 +++++ rewrite/parser_test.go | 56 ++++ rewrite/rewrite.go | 89 +++++ rewrite/util.go | 28 ++ rule/parser.go | 2 + rule/user_gent.go | 52 +++ test/go.mod | 8 +- test/go.sum | 16 +- tunnel/statistic/tracker.go | 3 +- tunnel/tunnel.go | 35 +- 40 files changed, 2144 insertions(+), 71 deletions(-) create mode 100644 adapter/inbound/mitm.go create mode 100644 adapter/outbound/mitm.go create mode 100644 common/cert/cert.go create mode 100644 common/cert/cert_test.go create mode 100644 common/cert/storage.go create mode 100644 constant/rewrite.go create mode 100644 listener/mitm/client.go create mode 100644 listener/mitm/proxy.go create mode 100644 listener/mitm/server.go create mode 100644 listener/mitm/session.go create mode 100644 listener/mitm/utils.go create mode 100644 rewrite/base.go create mode 100644 rewrite/handler.go create mode 100644 rewrite/parser.go create mode 100644 rewrite/parser_test.go create mode 100644 rewrite/rewrite.go create mode 100644 rewrite/util.go create mode 100644 rule/user_gent.go diff --git a/README.md b/README.md index 2eca8bbe..be6503cb 100644 --- a/README.md +++ b/README.md @@ -36,12 +36,44 @@ Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki). ## Advanced usage for this branch +### MITM configuration +A root CA certificate is required, the +MITM proxy server will generate a CA certificate file and a CA private key file in your Clash home directory, you can use your own certificate replace it. + +Need to install and trust the CA certificate on the client device, open this URL http://mitm.clash/cert.crt by the web browser to install the CA certificate, the host name 'mitm.clash' was always been hijacked. + +NOTE: this feature cannot work on tls pinning + +WARNING: DO NOT USE THIS FEATURE TO BREAK LOCAL LAWS + +```yaml +# Port of MITM proxy server on the local end +mitm-port: 7894 + +# Man-In-The-Middle attack +mitm: + hosts: # use for others proxy type. E.g: TUN, socks + - +.example.com + rules: # rewrite rules + - '^https?://www\.example\.com/1 url reject' # The "reject" returns HTTP status code 404 with no content. + - '^https?://www\.example\.com/2 url reject-200' # The "reject-200" returns HTTP status code 200 with no content. + - '^https?://www\.example\.com/3 url reject-img' # The "reject-img" returns HTTP status code 200 with content of 1px png. + - '^https?://www\.example\.com/4 url reject-dict' # The "reject-dict" returns HTTP status code 200 with content of empty json object. + - '^https?://www\.example\.com/5 url reject-array' # The "reject-array" returns HTTP status code 200 with content of empty json array. + - '^https?://www\.example\.com/(6) url 302 https://www.example.com/new-$1' + - '^https?://www\.(example)\.com/7 url 307 https://www.$1.com/new-7' + - '^https?://www\.example\.com/8 url request-header (\r\n)User-Agent:.+(\r\n) request-header $1User-Agent: haha-wriohoh$2' # The "request-header" works for all the http headers not just one single header, so you can match two or more headers including CRLF in one regular expression. + - '^https?://www\.example\.com/9 url request-body "pos_2":\[.*\],"pos_3" request-body "pos_2":[{"xx": "xx"}],"pos_3"' + - '^https?://www\.example\.com/10 url response-header (\r\n)Tracecode:.+(\r\n) response-header $1Tracecode: 88888888888$2' + - '^https?://www\.example\.com/11 url response-body "errmsg":"ok" response-body "errmsg":"not-ok"' +``` + ### DNS configuration Support resolve ip with a proxy tunnel. Support `geosite` with `fallback-filter`. -Use curl -X POST controllerip:port/cache/fakeip/flush to flush persistence fakeip +Use `curl -X POST controllerip:port/cache/fakeip/flush` to flush persistence fakeip ```yaml dns: enable: true @@ -85,6 +117,7 @@ tun: ``` ### Rules configuration - Support rule `GEOSITE`. +- Support rule `USER-AGENT`. - Support `multiport` condition for rule `SRC-PORT` and `DST-PORT`. - Support `network` condition for all rules. - Support `process` condition for all rules. @@ -104,7 +137,10 @@ rules: # multiport condition for rules SRC-PORT and DST-PORT - DST-PORT,123/136/137-139,DIRECT,udp - + + # USER-AGENT payload cannot include the comma character, '*' meaning any character. + - USER-AGENT,*example*,PROXY + # rule GEOSITE - GEOSITE,category-ads-all,REJECT - GEOSITE,icloud@cn,DIRECT diff --git a/adapter/inbound/mitm.go b/adapter/inbound/mitm.go new file mode 100644 index 00000000..db3645ab --- /dev/null +++ b/adapter/inbound/mitm.go @@ -0,0 +1,22 @@ +package inbound + +import ( + "net" + + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/context" + "github.com/Dreamacro/clash/transport/socks5" +) + +// NewMitm receive mitm request and return MitmContext +func NewMitm(target socks5.Addr, source net.Addr, userAgent string, conn net.Conn) *context.ConnContext { + metadata := parseSocksAddr(target) + metadata.NetWork = C.TCP + metadata.Type = C.MITM + metadata.UserAgent = userAgent + if ip, port, err := parseAddr(source.String()); err == nil { + metadata.SrcIP = ip + metadata.SrcPort = port + } + return context.NewConnContext(conn, metadata) +} diff --git a/adapter/outbound/http.go b/adapter/outbound/http.go index 44dc705a..ff87af6f 100644 --- a/adapter/outbound/http.go +++ b/adapter/outbound/http.go @@ -89,6 +89,10 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error { req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth))) } + if metadata.Type == C.MITM { + req.Header.Add("Origin-Request-Source-Address", metadata.SourceAddress()) + } + if err := req.Write(rw); err != nil { return err } diff --git a/adapter/outbound/mitm.go b/adapter/outbound/mitm.go new file mode 100644 index 00000000..80577cd9 --- /dev/null +++ b/adapter/outbound/mitm.go @@ -0,0 +1,68 @@ +package outbound + +import ( + "context" + "errors" + + "github.com/Dreamacro/clash/component/dialer" + "github.com/Dreamacro/clash/component/trie" + C "github.com/Dreamacro/clash/constant" + + "go.uber.org/atomic" +) + +var ( + errIgnored = errors.New("not match in mitm host lists") + httpProxyClient = NewHttp(HttpOption{}) + + MiddlemanServerAddress = atomic.NewString("") + MiddlemanRewriteHosts *trie.DomainTrie[bool] +) + +type Mitm struct { + *Base +} + +// DialContext implements C.ProxyAdapter +func (d *Mitm) DialContext(ctx context.Context, metadata *C.Metadata, _ ...dialer.Option) (C.Conn, error) { + addr := MiddlemanServerAddress.Load() + if addr == "" || MiddlemanRewriteHosts == nil { + return nil, errIgnored + } + + if MiddlemanRewriteHosts.Search(metadata.String()) == nil { + return nil, errIgnored + } + + metadata.Type = C.MITM + + if metadata.Host != "" { + metadata.AddrType = C.AtypDomainName + metadata.DstIP = nil + } + + c, err := dialer.DialContext(ctx, "tcp", addr, []dialer.Option{dialer.WithInterface(""), dialer.WithRoutingMark(0)}...) + if err != nil { + return nil, err + } + + tcpKeepAlive(c) + + defer safeConnClose(c, err) + + c, err = httpProxyClient.StreamConn(c, metadata) + if err != nil { + return nil, err + } + + return NewConn(c, d), nil +} + +func NewMitm() *Mitm { + return &Mitm{ + Base: &Base{ + name: "Mitm", + tp: C.Mitm, + }, + } +} diff --git a/common/cert/cert.go b/common/cert/cert.go new file mode 100644 index 00000000..3c931665 --- /dev/null +++ b/common/cert/cert.go @@ -0,0 +1,282 @@ +package cert + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net" + "os" + "sync/atomic" + "time" +) + +var currentSerialNumber = time.Now().Unix() + +type Config struct { + ca *x509.Certificate + caPrivateKey *rsa.PrivateKey + + roots *x509.CertPool + + privateKey *rsa.PrivateKey + + validity time.Duration + keyID []byte + organization string + + certsStorage CertsStorage +} + +type CertsStorage interface { + Get(key string) (*tls.Certificate, bool) + + Set(key string, cert *tls.Certificate) +} + +type CertsCache struct { + certsCache map[string]*tls.Certificate +} + +func (c *CertsCache) Get(key string) (*tls.Certificate, bool) { + v, ok := c.certsCache[key] + return v, ok +} + +func (c *CertsCache) Set(key string, cert *tls.Certificate) { + c.certsCache[key] = cert +} + +func NewAuthority(name, organization string, validity time.Duration) (*x509.Certificate, *rsa.PrivateKey, error) { + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, err + } + pub := privateKey.Public() + + pkixPub, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return nil, nil, err + } + h := sha1.New() + _, err = h.Write(pkixPub) + if err != nil { + return nil, nil, err + } + keyID := h.Sum(nil) + + serial := atomic.AddInt64(¤tSerialNumber, 1) + + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(serial), + Subject: pkix.Name{ + CommonName: name, + Organization: []string{organization}, + }, + SubjectKeyId: keyID, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + NotBefore: time.Now().Add(-validity), + NotAfter: time.Now().Add(validity), + DNSNames: []string{name}, + IsCA: true, + } + + raw, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, pub, privateKey) + if err != nil { + return nil, nil, err + } + + x509c, err := x509.ParseCertificate(raw) + if err != nil { + return nil, nil, err + } + + return x509c, privateKey, nil +} + +func NewConfig(ca *x509.Certificate, caPrivateKey *rsa.PrivateKey, storage CertsStorage) (*Config, error) { + roots := x509.NewCertPool() + roots.AddCert(ca) + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + pub := privateKey.Public() + + pkixPub, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return nil, err + } + h := sha1.New() + _, err = h.Write(pkixPub) + if err != nil { + return nil, err + } + keyID := h.Sum(nil) + + if storage == nil { + storage = &CertsCache{certsCache: make(map[string]*tls.Certificate)} + } + + return &Config{ + ca: ca, + caPrivateKey: caPrivateKey, + privateKey: privateKey, + keyID: keyID, + validity: time.Hour, + organization: "Clash", + certsStorage: storage, + roots: roots, + }, nil +} + +func (c *Config) GetCA() *x509.Certificate { + return c.ca +} + +func (c *Config) SetOrganization(organization string) { + c.organization = organization +} + +func (c *Config) SetValidity(validity time.Duration) { + c.validity = validity +} + +func (c *Config) NewTLSConfigForHost(hostname string) *tls.Config { + tlsConfig := &tls.Config{ + GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { + host := clientHello.ServerName + if host == "" { + host = hostname + } + + return c.GetOrCreateCert(host) + }, + NextProtos: []string{"http/1.1"}, + } + + tlsConfig.InsecureSkipVerify = true + + return tlsConfig +} + +func (c *Config) GetOrCreateCert(hostname string, ips ...net.IP) (*tls.Certificate, error) { + host, _, err := net.SplitHostPort(hostname) + if err == nil { + hostname = host + } + + tlsCertificate, ok := c.certsStorage.Get(hostname) + if ok { + if _, err = tlsCertificate.Leaf.Verify(x509.VerifyOptions{ + DNSName: hostname, + Roots: c.roots, + }); err == nil { + return tlsCertificate, nil + } + } + + serial := atomic.AddInt64(¤tSerialNumber, 1) + + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(serial), + Subject: pkix.Name{ + CommonName: hostname, + Organization: []string{c.organization}, + }, + SubjectKeyId: c.keyID, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + NotBefore: time.Now().Add(-c.validity), + NotAfter: time.Now().Add(c.validity), + } + + if ip := net.ParseIP(hostname); ip != nil { + ips = append(ips, ip) + } else { + tmpl.DNSNames = []string{hostname} + } + + tmpl.IPAddresses = ips + + raw, err := x509.CreateCertificate(rand.Reader, tmpl, c.ca, c.privateKey.Public(), c.caPrivateKey) + if err != nil { + return nil, err + } + + x509c, err := x509.ParseCertificate(raw) + if err != nil { + return nil, err + } + + tlsCertificate = &tls.Certificate{ + Certificate: [][]byte{raw, c.ca.Raw}, + PrivateKey: c.privateKey, + Leaf: x509c, + } + + c.certsStorage.Set(hostname, tlsCertificate) + return tlsCertificate, nil +} + +// GenerateAndSave generate CA private key and CA certificate and dump them to file +func GenerateAndSave(caPath string, caKeyPath string) error { + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return err + } + + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(time.Now().Unix()), + Subject: pkix.Name{ + Country: []string{"US"}, + CommonName: "Clash Root CA", + Organization: []string{"Clash Trust Services"}, + }, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + NotBefore: time.Now().Add(-(time.Hour * 24 * 60)), + NotAfter: time.Now().Add(time.Hour * 24 * 365 * 25), + BasicConstraintsValid: true, + IsCA: true, + } + + caRaw, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, privateKey.Public(), privateKey) + if err != nil { + return err + } + + caOut, err := os.OpenFile(caPath, os.O_CREATE|os.O_WRONLY, 0o600) + if err != nil { + return err + } + defer func(caOut *os.File) { + _ = caOut.Close() + }(caOut) + + if err = pem.Encode(caOut, &pem.Block{Type: "CERTIFICATE", Bytes: caRaw}); err != nil { + return err + } + + caKeyOut, err := os.OpenFile(caKeyPath, os.O_CREATE|os.O_WRONLY, 0o600) + if err != nil { + return err + } + defer func(caKeyOut *os.File) { + _ = caKeyOut.Close() + }(caKeyOut) + + if err = pem.Encode(caKeyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}); err != nil { + return err + } + + return nil +} diff --git a/common/cert/cert_test.go b/common/cert/cert_test.go new file mode 100644 index 00000000..42265613 --- /dev/null +++ b/common/cert/cert_test.go @@ -0,0 +1,76 @@ +package cert + +import ( + "crypto/tls" + "crypto/x509" + "net" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestCert(t *testing.T) { + ca, privateKey, err := NewAuthority("Clash ca", "Clash", 24*time.Hour) + + assert.Nil(t, err) + assert.NotNil(t, ca) + assert.NotNil(t, privateKey) + + c, err := NewConfig(ca, privateKey, nil) + assert.Nil(t, err) + + c.SetValidity(20 * time.Hour) + c.SetOrganization("Test Organization") + + conf := c.NewTLSConfigForHost("example.org") + assert.Equal(t, []string{"http/1.1"}, conf.NextProtos) + assert.True(t, conf.InsecureSkipVerify) + + // Test generating a certificate + clientHello := &tls.ClientHelloInfo{ + ServerName: "example.org", + } + tlsCert, err := conf.GetCertificate(clientHello) + assert.Nil(t, err) + assert.NotNil(t, tlsCert) + + // Assert certificate details + x509c := tlsCert.Leaf + assert.Equal(t, "example.org", x509c.Subject.CommonName) + assert.Nil(t, x509c.VerifyHostname("example.org")) + assert.Equal(t, []string{"Test Organization"}, x509c.Subject.Organization) + assert.NotNil(t, x509c.SubjectKeyId) + assert.True(t, x509c.BasicConstraintsValid) + assert.True(t, x509c.KeyUsage&x509.KeyUsageKeyEncipherment == x509.KeyUsageKeyEncipherment) + assert.True(t, x509c.KeyUsage&x509.KeyUsageDigitalSignature == x509.KeyUsageDigitalSignature) + assert.Equal(t, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, x509c.ExtKeyUsage) + assert.Equal(t, []string{"example.org"}, x509c.DNSNames) + assert.True(t, x509c.NotBefore.Before(time.Now().Add(-2*time.Hour))) + assert.True(t, x509c.NotAfter.After(time.Now().Add(2*time.Hour))) + + // Check that certificate is cached + tlsCert2, err := c.GetOrCreateCert("example.org") + assert.Nil(t, err) + assert.True(t, tlsCert == tlsCert2) + + // Check the certificate for an IP + tlsCertForIP, err := c.GetOrCreateCert("192.168.0.1:443") + assert.Nil(t, err) + x509c = tlsCertForIP.Leaf + assert.Equal(t, 1, len(x509c.IPAddresses)) + assert.True(t, net.ParseIP("192.168.0.1").Equal(x509c.IPAddresses[0])) +} + +func TestGenerateAndSave(t *testing.T) { + caPath := "ca.crt" + caKeyPath := "ca.key" + + err := GenerateAndSave(caPath, caKeyPath) + + assert.Nil(t, err) + + _ = os.Remove(caPath) + _ = os.Remove(caKeyPath) +} diff --git a/common/cert/storage.go b/common/cert/storage.go new file mode 100644 index 00000000..61663e73 --- /dev/null +++ b/common/cert/storage.go @@ -0,0 +1,32 @@ +package cert + +import ( + "crypto/tls" + "time" + + "github.com/Dreamacro/clash/common/cache" +) + +var TTL = time.Hour * 2 + +// AutoGCCertsStorage cache with the generated certificates, auto released after TTL +type AutoGCCertsStorage struct { + certsCache *cache.Cache[string, *tls.Certificate] +} + +// Get gets the certificate from the storage +func (c *AutoGCCertsStorage) Get(key string) (*tls.Certificate, bool) { + ca := c.certsCache.Get(key) + return ca, ca != nil +} + +// Set saves the certificate to the storage +func (c *AutoGCCertsStorage) Set(key string, cert *tls.Certificate) { + c.certsCache.Put(key, cert, TTL) +} + +func NewAutoGCCertsStorage() *AutoGCCertsStorage { + return &AutoGCCertsStorage{ + certsCache: cache.New[string, *tls.Certificate](TTL), + } +} diff --git a/component/geodata/memconservative/cache.go b/component/geodata/memconservative/cache.go index 2981e5c0..3a94d352 100644 --- a/component/geodata/memconservative/cache.go +++ b/component/geodata/memconservative/cache.go @@ -33,7 +33,7 @@ func (g GeoIPCache) Set(key string, value *router.GeoIP) { } func (g GeoIPCache) Unmarshal(filename, code string) (*router.GeoIP, error) { - asset := C.Path.GetAssetLocation(filename) + asset := C.Path.Resolve(filename) idx := strings.ToLower(asset + ":" + code) if g.Has(idx) { return g.Get(idx), nil @@ -98,7 +98,7 @@ func (g GeoSiteCache) Set(key string, value *router.GeoSite) { } func (g GeoSiteCache) Unmarshal(filename, code string) (*router.GeoSite, error) { - asset := C.Path.GetAssetLocation(filename) + asset := C.Path.Resolve(filename) idx := strings.ToLower(asset + ":" + code) if g.Has(idx) { return g.Get(idx), nil diff --git a/component/geodata/standard/standard.go b/component/geodata/standard/standard.go index 0febbc08..86a5791d 100644 --- a/component/geodata/standard/standard.go +++ b/component/geodata/standard/standard.go @@ -26,7 +26,7 @@ func ReadFile(path string) ([]byte, error) { } func ReadAsset(file string) ([]byte, error) { - return ReadFile(C.Path.GetAssetLocation(file)) + return ReadFile(C.Path.Resolve(file)) } func loadIP(filename, country string) ([]*router.CIDR, error) { diff --git a/config/config.go b/config/config.go index eedc1959..e86a5e42 100644 --- a/config/config.go +++ b/config/config.go @@ -25,6 +25,7 @@ import ( "github.com/Dreamacro/clash/dns" "github.com/Dreamacro/clash/listener/tun/ipstack/commons" "github.com/Dreamacro/clash/log" + rewrites "github.com/Dreamacro/clash/rewrite" R "github.com/Dreamacro/clash/rule" T "github.com/Dreamacro/clash/tunnel" @@ -49,6 +50,7 @@ type Inbound struct { RedirPort int `json:"redir-port"` TProxyPort int `json:"tproxy-port"` MixedPort int `json:"mixed-port"` + MitmPort int `json:"mitm-port"` Authentication []string `json:"authentication"` AllowLan bool `json:"allow-lan"` BindAddress string `json:"bind-address"` @@ -72,7 +74,7 @@ type DNS struct { EnhancedMode C.DNSMode `yaml:"enhanced-mode"` DefaultNameserver []dns.NameServer `yaml:"default-nameserver"` FakeIPRange *fakeip.Pool - Hosts *trie.DomainTrie + Hosts *trie.DomainTrie[netip.Addr] NameServerPolicy map[string]dns.NameServer ProxyServerNameserver []dns.NameServer } @@ -107,6 +109,12 @@ type IPTables struct { InboundInterface string `yaml:"inbound-interface" json:"inbound-interface"` } +// Mitm config +type Mitm struct { + Hosts *trie.DomainTrie[bool] `yaml:"hosts" json:"hosts"` + Rules C.RewriteRule `yaml:"rules" json:"rules"` +} + // Experimental config type Experimental struct{} @@ -115,9 +123,10 @@ type Config struct { General *General Tun *Tun IPTables *IPTables + Mitm *Mitm DNS *DNS Experimental *Experimental - Hosts *trie.DomainTrie + Hosts *trie.DomainTrie[netip.Addr] Profile *Profile Rules []C.Rule Users []auth.AuthUser @@ -157,12 +166,18 @@ type RawTun struct { AutoRoute bool `yaml:"auto-route" json:"auto-route"` } +type RawMitm struct { + Hosts []string `yaml:"hosts" json:"hosts"` + Rules []string `yaml:"rules" json:"rules"` +} + type RawConfig struct { Port int `yaml:"port"` SocksPort int `yaml:"socks-port"` RedirPort int `yaml:"redir-port"` TProxyPort int `yaml:"tproxy-port"` MixedPort int `yaml:"mixed-port"` + MitmPort int `yaml:"mitm-port"` Authentication []string `yaml:"authentication"` AllowLan bool `yaml:"allow-lan"` BindAddress string `yaml:"bind-address"` @@ -180,6 +195,7 @@ type RawConfig struct { DNS RawDNS `yaml:"dns"` Tun RawTun `yaml:"tun"` IPTables IPTables `yaml:"iptables"` + MITM RawMitm `yaml:"mitm"` Experimental Experimental `yaml:"experimental"` Profile Profile `yaml:"profile"` Proxy []map[string]any `yaml:"proxies"` @@ -240,6 +256,10 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { "tls://223.5.5.5:853", }, }, + MITM: RawMitm{ + Hosts: []string{}, + Rules: []string{}, + }, Profile: Profile{ StoreSelected: true, }, @@ -298,6 +318,12 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { } config.DNS = dnsCfg + mitm, err := parseMitm(rawCfg.MITM) + if err != nil { + return nil, err + } + config.Mitm = mitm + config.Users = parseAuthentication(rawCfg.Authentication) return config, nil @@ -322,6 +348,7 @@ func parseGeneral(cfg *RawConfig) (*General, error) { RedirPort: cfg.RedirPort, TProxyPort: cfg.TProxyPort, MixedPort: cfg.MixedPort, + MitmPort: cfg.MitmPort, AllowLan: cfg.AllowLan, BindAddress: cfg.BindAddress, }, @@ -501,24 +528,29 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, error) { return rules, nil } -func parseHosts(cfg *RawConfig) (*trie.DomainTrie, error) { - tree := trie.New() +func parseHosts(cfg *RawConfig) (*trie.DomainTrie[netip.Addr], error) { + tree := trie.New[netip.Addr]() // add default hosts - if err := tree.Insert("localhost", net.IP{127, 0, 0, 1}); err != nil { + if err := tree.Insert("localhost", netip.AddrFrom4([4]byte{127, 0, 0, 1})); err != nil { log.Errorln("insert localhost to host error: %s", err.Error()) } if len(cfg.Hosts) != 0 { for domain, ipStr := range cfg.Hosts { - ip := net.ParseIP(ipStr) - if ip == nil { + ip, err := netip.ParseAddr(ipStr) + if err != nil { return nil, fmt.Errorf("%s is not a valid IP", ipStr) } _ = tree.Insert(domain, ip) } } + // add mitm.clash hosts + if err := tree.Insert("mitm.clash", netip.AddrFrom4([4]byte{8, 8, 9, 9})); err != nil { + log.Errorln("insert mitm.clash to host error: %s", err.Error()) + } + return tree, nil } @@ -652,7 +684,7 @@ func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]*router.DomainM return sites, nil } -func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS, error) { +func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[netip.Addr], rules []C.Rule) (*DNS, error) { cfg := rawCfg.DNS if cfg.Enable && len(cfg.NameServer) == 0 { return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty") @@ -705,10 +737,10 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS, return nil, err } - var host *trie.DomainTrie + var host *trie.DomainTrie[bool] // fake ip skip host filter if len(cfg.FakeIPFilter) != 0 { - host = trie.New() + host = trie.New[bool]() for _, domain := range cfg.FakeIPFilter { _ = host.Insert(domain, true) } @@ -716,7 +748,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS, if len(dnsCfg.Fallback) != 0 { if host == nil { - host = trie.New() + host = trie.New[bool]() } for _, fb := range dnsCfg.Fallback { if net.ParseIP(fb.Addr) != nil { @@ -803,3 +835,38 @@ func parseTun(rawTun RawTun, general *General) (*Tun, error) { AutoRoute: rawTun.AutoRoute, }, nil } + +func parseMitm(rawMitm RawMitm) (*Mitm, error) { + var ( + req []C.Rewrite + res []C.Rewrite + ) + + for _, line := range rawMitm.Rules { + rule, err := rewrites.ParseRewrite(line) + if err != nil { + return nil, fmt.Errorf("parse rewrite rule failure: %w", err) + } + + if rule.RuleType() == C.MitmResponseHeader || rule.RuleType() == C.MitmResponseBody { + res = append(res, rule) + } else { + req = append(req, rule) + } + } + + hosts := trie.New[bool]() + + if len(rawMitm.Hosts) != 0 { + for _, domain := range rawMitm.Hosts { + _ = hosts.Insert(domain, true) + } + } + + _ = hosts.Insert("mitm.clash", true) + + return &Mitm{ + Hosts: hosts, + Rules: rewrites.NewRewriteRules(req, res), + }, nil +} diff --git a/constant/adapters.go b/constant/adapters.go index 2898d9c7..40849422 100644 --- a/constant/adapters.go +++ b/constant/adapters.go @@ -13,6 +13,7 @@ import ( const ( Direct AdapterType = iota Reject + Mitm Shadowsocks ShadowsocksR @@ -129,6 +130,8 @@ func (at AdapterType) String() string { return "Direct" case Reject: return "Reject" + case Mitm: + return "Mitm" case Shadowsocks: return "Shadowsocks" diff --git a/constant/metadata.go b/constant/metadata.go index 3da67201..70ed909b 100644 --- a/constant/metadata.go +++ b/constant/metadata.go @@ -23,6 +23,7 @@ const ( REDIR TPROXY TUN + MITM ) type NetWork int @@ -58,6 +59,8 @@ func (t Type) String() string { return "TProxy" case TUN: return "Tun" + case MITM: + return "Mitm" default: return "Unknown" } @@ -80,6 +83,7 @@ type Metadata struct { DNSMode DNSMode `json:"dnsMode"` Process string `json:"process"` ProcessPath string `json:"processPath"` + UserAgent string `json:"userAgent"` } func (m *Metadata) RemoteAddress() string { diff --git a/constant/path.go b/constant/path.go index 4580b25c..b3167edc 100644 --- a/constant/path.go +++ b/constant/path.go @@ -71,6 +71,10 @@ func (p *path) GeoSite() string { return P.Join(p.homeDir, "geosite.dat") } -func (p *path) GetAssetLocation(file string) string { - return P.Join(p.homeDir, file) +func (p *path) RootCA() string { + return p.Resolve("mitm_ca.crt") +} + +func (p *path) CAKey() string { + return p.Resolve("mitm_ca.key") } diff --git a/constant/rewrite.go b/constant/rewrite.go new file mode 100644 index 00000000..06adde35 --- /dev/null +++ b/constant/rewrite.go @@ -0,0 +1,82 @@ +package constant + +import ( + "regexp" +) + +var RewriteTypeMapping = map[string]RewriteType{ + MitmReject.String(): MitmReject, + MitmReject200.String(): MitmReject200, + MitmRejectImg.String(): MitmRejectImg, + MitmRejectDict.String(): MitmRejectDict, + MitmRejectArray.String(): MitmRejectArray, + Mitm302.String(): Mitm302, + Mitm307.String(): Mitm307, + MitmRequestHeader.String(): MitmRequestHeader, + MitmRequestBody.String(): MitmRequestBody, + MitmResponseHeader.String(): MitmResponseHeader, + MitmResponseBody.String(): MitmResponseBody, +} + +const ( + MitmReject RewriteType = iota + MitmReject200 + MitmRejectImg + MitmRejectDict + MitmRejectArray + + Mitm302 + Mitm307 + + MitmRequestHeader + MitmRequestBody + + MitmResponseHeader + MitmResponseBody +) + +type RewriteType int + +func (rt RewriteType) String() string { + switch rt { + case MitmReject: + return "reject" // 404 + case MitmReject200: + return "reject-200" + case MitmRejectImg: + return "reject-img" + case MitmRejectDict: + return "reject-dict" + case MitmRejectArray: + return "reject-array" + case Mitm302: + return "302" + case Mitm307: + return "307" + case MitmRequestHeader: + return "request-header" + case MitmRequestBody: + return "request-body" + case MitmResponseHeader: + return "response-header" + case MitmResponseBody: + return "response-body" + default: + return "Unknown" + } +} + +type Rewrite interface { + ID() string + URLRegx() *regexp.Regexp + RuleType() RewriteType + RuleRegx() *regexp.Regexp + RulePayload() string + ReplaceURLPayload([]string) string + ReplaceSubPayload(string) string +} + +type RewriteRule interface { + SearchInRequest(func(Rewrite) bool) bool + SearchInResponse(func(Rewrite) bool) bool +} diff --git a/constant/rule.go b/constant/rule.go index 23f421aa..d59658c9 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -13,6 +13,7 @@ const ( DstPort Process ProcessPath + UserAgent MATCH ) @@ -42,6 +43,8 @@ func (rt RuleType) String() string { return "Process" case ProcessPath: return "ProcessPath" + case UserAgent: + return "UserAgent" case MATCH: return "Match" default: diff --git a/dns/middleware.go b/dns/middleware.go index 7259df66..4091fa9e 100644 --- a/dns/middleware.go +++ b/dns/middleware.go @@ -21,7 +21,7 @@ type ( middleware func(next handler) handler ) -func withHosts(hosts *trie.DomainTrie[netip.Addr]) middleware { +func withHosts(hosts *trie.DomainTrie[netip.Addr], mapping *cache.LruCache[string, string]) middleware { return func(next handler) handler { return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) { q := r.Question[0] @@ -30,23 +30,28 @@ func withHosts(hosts *trie.DomainTrie[netip.Addr]) middleware { return next(ctx, r) } - record := hosts.Search(strings.TrimRight(q.Name, ".")) + qName := strings.TrimRight(q.Name, ".") + record := hosts.Search(qName) if record == nil { return next(ctx, r) } ip := record.Data + if mapping != nil { + mapping.SetWithExpire(ip.Unmap().String(), qName, time.Now().Add(time.Second*5)) + } + msg := r.Copy() if ip.Is4() && q.Qtype == D.TypeA { rr := &D.A{} - rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeA, Class: D.ClassINET, Ttl: dnsDefaultTTL} + rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeA, Class: D.ClassINET, Ttl: 1} rr.A = ip.AsSlice() msg.Answer = []D.RR{rr} } else if ip.Is6() && q.Qtype == D.TypeAAAA { rr := &D.AAAA{} - rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeAAAA, Class: D.ClassINET, Ttl: dnsDefaultTTL} + rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeAAAA, Class: D.ClassINET, Ttl: 1} rr.AAAA = ip.AsSlice() msg.Answer = []D.RR{rr} @@ -177,7 +182,7 @@ func NewHandler(resolver *Resolver, mapper *ResolverEnhancer) handler { middlewares := []middleware{} if resolver.hosts != nil { - middlewares = append(middlewares, withHosts(resolver.hosts)) + middlewares = append(middlewares, withHosts(resolver.hosts, mapper.mapping)) } if mapper.mode == C.DNSFakeIP { diff --git a/go.mod b/go.mod index 5f4d8fbd..31d291dc 100644 --- a/go.mod +++ b/go.mod @@ -18,10 +18,11 @@ require ( go.etcd.io/bbolt v1.3.6 go.uber.org/atomic v1.9.0 go.uber.org/automaxprocs v1.4.0 - golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd - golang.org/x/net v0.0.0-20220225172249-27dd8689420f + golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 + golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 + golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f + golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 golang.zx2c4.com/wireguard v0.0.0-20220318042302-193cf8d6a5d6 golang.zx2c4.com/wireguard/windows v0.5.4-0.20220317000008-6432784c2469 @@ -37,8 +38,7 @@ require ( github.com/oschwald/maxminddb-golang v1.8.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect - golang.org/x/mod v0.5.1 // indirect - golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab // indirect + golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 // indirect golang.org/x/tools v0.1.9 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect diff --git a/go.sum b/go.sum index 40b7a038..479f39bf 100644 --- a/go.sum +++ b/go.sum @@ -81,11 +81,11 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38= -golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 h1:iU7T1X1J6yxDr0rda54sWGkHgOp5XJrqm79gcNlC2VM= +golang.org/x/crypto v0.0.0-20220408190544-5352b0902921/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 h1:LQmS1nU0twXLA96Kt7U9qtHJEbBk3z6Q0V4UXjZkpr4= +golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -99,8 +99,8 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 h1:EN5+DfgmRMvRUrMGERW2gQl3Vc+Z7ZMnI/xdEpPSf0c= +golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -125,8 +125,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 h1:A9i04dxx7Cribqbs8jf3FQLogkL/CV2YN7hj9KWJCkc= -golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw= +golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/hub/executor/executor.go b/hub/executor/executor.go index 887c473a..5166ba04 100644 --- a/hub/executor/executor.go +++ b/hub/executor/executor.go @@ -3,12 +3,14 @@ package executor import ( "fmt" "net" + "net/netip" "os" "runtime" "strconv" "sync" "github.com/Dreamacro/clash/adapter" + "github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outboundgroup" "github.com/Dreamacro/clash/component/auth" "github.com/Dreamacro/clash/component/dialer" @@ -71,12 +73,17 @@ func ApplyConfig(cfg *config.Config, force bool) { mux.Lock() defer mux.Unlock() - log.SetLevel(log.DEBUG) + if cfg.General.LogLevel == log.DEBUG { + log.SetLevel(log.DEBUG) + } else { + log.SetLevel(log.INFO) + } updateUsers(cfg.Users) updateProxies(cfg.Proxies, cfg.Providers) updateRules(cfg.Rules) updateHosts(cfg.Hosts) + updateMitm(cfg.Mitm) updateProfile(cfg) updateDNS(cfg.DNS, cfg.Tun) updateGeneral(cfg.General, force) @@ -101,6 +108,7 @@ func GetGeneral() *config.General { RedirPort: ports.RedirPort, TProxyPort: ports.TProxyPort, MixedPort: ports.MixedPort, + MitmPort: ports.MitmPort, Authentication: authenticator, AllowLan: P.AllowLan(), BindAddress: P.BindAddress(), @@ -168,7 +176,7 @@ func updateDNS(c *config.DNS, t *config.Tun) { } } -func updateHosts(tree *trie.DomainTrie) { +func updateHosts(tree *trie.DomainTrie[netip.Addr]) { resolver.DefaultHosts = tree } @@ -225,6 +233,7 @@ func updateGeneral(general *config.General, force bool) { P.ReCreateRedir(general.RedirPort, tcpIn, udpIn) P.ReCreateTProxy(general.TProxyPort, tcpIn, udpIn) P.ReCreateMixed(general.MixedPort, tcpIn, udpIn) + P.ReCreateMitm(general.MitmPort, tcpIn) } func updateUsers(users []auth.AuthUser) { @@ -330,6 +339,11 @@ func updateIPTables(cfg *config.Config) { log.Infoln("[IPTABLES] Setting iptables completed") } +func updateMitm(mitm *config.Mitm) { + outbound.MiddlemanRewriteHosts = mitm.Hosts + tunnel.UpdateRewrites(mitm.Rules) +} + func Shutdown() { P.Cleanup() tproxy.CleanupTProxyIPTables() diff --git a/hub/route/configs.go b/hub/route/configs.go index 3e36c054..a930c32b 100644 --- a/hub/route/configs.go +++ b/hub/route/configs.go @@ -30,6 +30,7 @@ type configSchema struct { RedirPort *int `json:"redir-port"` TProxyPort *int `json:"tproxy-port"` MixedPort *int `json:"mixed-port"` + MitmPort *int `json:"mitm-port"` Tun *config.Tun `json:"tun"` AllowLan *bool `json:"allow-lan"` BindAddress *string `json:"bind-address"` @@ -77,6 +78,7 @@ func patchConfigs(w http.ResponseWriter, r *http.Request) { P.ReCreateRedir(pointerOrDefault(general.RedirPort, ports.RedirPort), tcpIn, udpIn) P.ReCreateTProxy(pointerOrDefault(general.TProxyPort, ports.TProxyPort), tcpIn, udpIn) P.ReCreateMixed(pointerOrDefault(general.MixedPort, ports.MixedPort), tcpIn, udpIn) + P.ReCreateMitm(pointerOrDefault(general.MitmPort, ports.MitmPort), tcpIn) if general.Mode != nil { tunnel.SetMode(*general.Mode) diff --git a/listener/http/proxy.go b/listener/http/proxy.go index e8a805a9..d29f80f5 100644 --- a/listener/http/proxy.go +++ b/listener/http/proxy.go @@ -42,7 +42,7 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.Cache[string, var resp *http.Response if !trusted { - resp = authenticate(request, cache) + resp = Authenticate(request, cache) trusted = resp == nil } @@ -66,19 +66,19 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.Cache[string, request.RequestURI = "" - removeHopByHopHeaders(request.Header) - removeExtraHTTPHostPort(request) + RemoveHopByHopHeaders(request.Header) + RemoveExtraHTTPHostPort(request) if request.URL.Scheme == "" || request.URL.Host == "" { - resp = responseWith(request, http.StatusBadRequest) + resp = ResponseWith(request, http.StatusBadRequest) } else { resp, err = client.Do(request) if err != nil { - resp = responseWith(request, http.StatusBadGateway) + resp = ResponseWith(request, http.StatusBadGateway) } } - removeHopByHopHeaders(resp.Header) + RemoveHopByHopHeaders(resp.Header) } if keepAlive { @@ -98,12 +98,12 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.Cache[string, conn.Close() } -func authenticate(request *http.Request, cache *cache.Cache[string, bool]) *http.Response { +func Authenticate(request *http.Request, cache *cache.Cache[string, bool]) *http.Response { authenticator := authStore.Authenticator() if authenticator != nil { credential := parseBasicProxyAuthorization(request) if credential == "" { - resp := responseWith(request, http.StatusProxyAuthRequired) + resp := ResponseWith(request, http.StatusProxyAuthRequired) resp.Header.Set("Proxy-Authenticate", "Basic") return resp } @@ -117,14 +117,14 @@ func authenticate(request *http.Request, cache *cache.Cache[string, bool]) *http if !authed { log.Infoln("Auth failed from %s", request.RemoteAddr) - return responseWith(request, http.StatusForbidden) + return ResponseWith(request, http.StatusForbidden) } } return nil } -func responseWith(request *http.Request, statusCode int) *http.Response { +func ResponseWith(request *http.Request, statusCode int) *http.Response { return &http.Response{ StatusCode: statusCode, Status: http.StatusText(statusCode), diff --git a/listener/http/utils.go b/listener/http/utils.go index 74b12005..0e7c7535 100644 --- a/listener/http/utils.go +++ b/listener/http/utils.go @@ -8,8 +8,8 @@ import ( "strings" ) -// removeHopByHopHeaders remove hop-by-hop header -func removeHopByHopHeaders(header http.Header) { +// RemoveHopByHopHeaders remove hop-by-hop header +func RemoveHopByHopHeaders(header http.Header) { // Strip hop-by-hop header based on RFC: // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1 // https://www.mnot.net/blog/2011/07/11/what_proxies_must_do @@ -32,9 +32,9 @@ func removeHopByHopHeaders(header http.Header) { } } -// removeExtraHTTPHostPort remove extra host port (example.com:80 --> example.com) +// RemoveExtraHTTPHostPort remove extra host port (example.com:80 --> example.com) // It resolves the behavior of some HTTP servers that do not handle host:80 (e.g. baidu.com) -func removeExtraHTTPHostPort(req *http.Request) { +func RemoveExtraHTTPHostPort(req *http.Request) { host := req.Host if host == "" { host = req.URL.Host diff --git a/listener/listener.go b/listener/listener.go index 46157e5d..1bda3a8d 100644 --- a/listener/listener.go +++ b/listener/listener.go @@ -1,6 +1,9 @@ package proxy import ( + "crypto/rsa" + "crypto/tls" + "crypto/x509" "fmt" "net" "os" @@ -8,9 +11,12 @@ import ( "sync" "github.com/Dreamacro/clash/adapter/inbound" + "github.com/Dreamacro/clash/adapter/outbound" + "github.com/Dreamacro/clash/common/cert" "github.com/Dreamacro/clash/config" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/listener/http" + "github.com/Dreamacro/clash/listener/mitm" "github.com/Dreamacro/clash/listener/mixed" "github.com/Dreamacro/clash/listener/redir" "github.com/Dreamacro/clash/listener/socks" @@ -18,6 +24,8 @@ import ( "github.com/Dreamacro/clash/listener/tun" "github.com/Dreamacro/clash/listener/tun/ipstack" "github.com/Dreamacro/clash/log" + rewrites "github.com/Dreamacro/clash/rewrite" + "github.com/Dreamacro/clash/tunnel" ) var ( @@ -34,6 +42,7 @@ var ( mixedListener *mixed.Listener mixedUDPLister *socks.UDPListener tunStackListener ipstack.Stack + mitmListener *mitm.Listener // lock for recreate function socksMux sync.Mutex @@ -42,6 +51,7 @@ var ( tproxyMux sync.Mutex mixedMux sync.Mutex tunMux sync.Mutex + mitmMux sync.Mutex ) type Ports struct { @@ -50,6 +60,7 @@ type Ports struct { RedirPort int `json:"redir-port"` TProxyPort int `json:"tproxy-port"` MixedPort int `json:"mixed-port"` + MitmPort int `json:"mitm-port"` } func AllowLan() bool { @@ -331,6 +342,85 @@ func ReCreateTun(tunConf *config.Tun, tunAddressPrefix string, tcpIn chan<- C.Co tunStackListener, err = tun.New(tunConf, tunAddressPrefix, tcpIn, udpIn) } +func ReCreateMitm(port int, tcpIn chan<- C.ConnContext) { + mitmMux.Lock() + defer mitmMux.Unlock() + + var err error + defer func() { + if err != nil { + log.Errorln("Start MITM server error: %s", err.Error()) + } + }() + + addr := genAddr(bindAddress, port, allowLan) + + if mitmListener != nil { + if mitmListener.RawAddress() == addr { + return + } + outbound.MiddlemanServerAddress.Store("") + tunnel.MitmOutbound = nil + _ = mitmListener.Close() + mitmListener = nil + } + + if portIsZero(addr) { + return + } + + if err = initCert(); err != nil { + return + } + + var ( + rootCACert tls.Certificate + x509c *x509.Certificate + certOption *cert.Config + ) + + rootCACert, err = tls.LoadX509KeyPair(C.Path.RootCA(), C.Path.CAKey()) + if err != nil { + return + } + + privateKey := rootCACert.PrivateKey.(*rsa.PrivateKey) + + x509c, err = x509.ParseCertificate(rootCACert.Certificate[0]) + if err != nil { + return + } + + certOption, err = cert.NewConfig( + x509c, + privateKey, + cert.NewAutoGCCertsStorage(), + ) + if err != nil { + return + } + + certOption.SetValidity(cert.TTL << 3) + certOption.SetOrganization("Clash ManInTheMiddle Proxy Services") + + opt := &mitm.Option{ + Addr: addr, + ApiHost: "mitm.clash", + CertConfig: certOption, + Handler: &rewrites.RewriteHandler{}, + } + + mitmListener, err = mitm.New(opt, tcpIn) + if err != nil { + return + } + + outbound.MiddlemanServerAddress.Store(mitmListener.Address()) + tunnel.MitmOutbound = outbound.NewMitm() + + log.Infoln("Mitm proxy listening at: %s", mitmListener.Address()) +} + // GetPorts return the ports of proxy servers func GetPorts() *Ports { ports := &Ports{} @@ -365,6 +455,12 @@ func GetPorts() *Ports { ports.MixedPort = port } + if mitmListener != nil { + _, portStr, _ := net.SplitHostPort(mitmListener.Address()) + port, _ := strconv.Atoi(portStr) + ports.MitmPort = port + } + return ports } @@ -387,6 +483,19 @@ func genAddr(host string, port int, allowLan bool) string { return fmt.Sprintf("127.0.0.1:%d", port) } +func initCert() error { + if _, err := os.Stat(C.Path.RootCA()); os.IsNotExist(err) { + log.Infoln("Can't find mitm_ca.crt, start generate") + err = cert.GenerateAndSave(C.Path.RootCA(), C.Path.CAKey()) + if err != nil { + return err + } + log.Infoln("Generated CA private key and CA certificate finish") + } + + return nil +} + func Cleanup() { if tunStackListener != nil { _ = tunStackListener.Close() diff --git a/listener/mitm/client.go b/listener/mitm/client.go new file mode 100644 index 00000000..278de173 --- /dev/null +++ b/listener/mitm/client.go @@ -0,0 +1,54 @@ +package mitm + +import ( + "context" + "crypto/tls" + "errors" + "net" + "net/http" + "time" + + "github.com/Dreamacro/clash/adapter/inbound" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/socks5" +) + +var ErrCertUnsupported = errors.New("tls: client cert unsupported") + +func newClient(source net.Addr, userAgent string, in chan<- C.ConnContext) *http.Client { + return &http.Client{ + Transport: &http.Transport{ + // excepted HTTP/2 + TLSNextProto: make(map[string]func(string, *tls.Conn) http.RoundTripper), + // from http.DefaultTransport + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + TLSClientConfig: &tls.Config{ + GetClientCertificate: func(info *tls.CertificateRequestInfo) (certificate *tls.Certificate, e error) { + return nil, ErrCertUnsupported + }, + }, + DialContext: func(context context.Context, network, address string) (net.Conn, error) { + if network != "tcp" && network != "tcp4" && network != "tcp6" { + return nil, errors.New("unsupported network " + network) + } + + dstAddr := socks5.ParseAddr(address) + if dstAddr == nil { + return nil, socks5.ErrAddressNotSupported + } + + left, right := net.Pipe() + + in <- inbound.NewMitm(dstAddr, source, userAgent, right) + + return left, nil + }, + }, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } +} diff --git a/listener/mitm/proxy.go b/listener/mitm/proxy.go new file mode 100644 index 00000000..a0d3bab6 --- /dev/null +++ b/listener/mitm/proxy.go @@ -0,0 +1,357 @@ +package mitm + +import ( + "bytes" + "crypto/tls" + "encoding/pem" + "errors" + "fmt" + "io" + "net" + "net/http" + "strconv" + "strings" + "time" + + "github.com/Dreamacro/clash/adapter/inbound" + "github.com/Dreamacro/clash/common/cache" + N "github.com/Dreamacro/clash/common/net" + C "github.com/Dreamacro/clash/constant" + httpL "github.com/Dreamacro/clash/listener/http" +) + +func HandleConn(c net.Conn, opt *Option, in chan<- C.ConnContext, cache *cache.Cache[string, bool]) { + var ( + source net.Addr + client *http.Client + ) + + defer func() { + if client != nil { + client.CloseIdleConnections() + } + }() + +startOver: + if tc, ok := c.(*net.TCPConn); ok { + _ = tc.SetKeepAlive(true) + } + + var conn *N.BufferedConn + if bufConn, ok := c.(*N.BufferedConn); ok { + conn = bufConn + } else { + conn = N.NewBufferedConn(c) + } + + trusted := cache == nil // disable authenticate if cache is nil + +readLoop: + for { + _ = conn.SetDeadline(time.Now().Add(30 * time.Second)) // use SetDeadline instead of Proxy-Connection keep-alive + + request, err := httpL.ReadRequest(conn.Reader()) + if err != nil { + handleError(opt, nil, err) + break readLoop + } + + var response *http.Response + + session := NewSession(conn, request, response) + + source = parseSourceAddress(session.request, c, source) + request.RemoteAddr = source.String() + + if !trusted { + response = httpL.Authenticate(request, cache) + + trusted = response == nil + } + + if trusted { + if session.request.Method == http.MethodConnect { + // Manual writing to support CONNECT for http 1.0 (workaround for uplay client) + if _, err = fmt.Fprintf(session.conn, "HTTP/%d.%d %03d %s\r\n\r\n", session.request.ProtoMajor, session.request.ProtoMinor, http.StatusOK, "Connection established"); err != nil { + handleError(opt, session, err) + break readLoop // close connection + } + + if couldBeWithManInTheMiddleAttack(session.request.URL.Host, opt) { + b := make([]byte, 1) + if _, err = session.conn.Read(b); err != nil { + handleError(opt, session, err) + break readLoop // close connection + } + + buf := make([]byte, session.conn.(*N.BufferedConn).Buffered()) + _, _ = session.conn.Read(buf) + + mc := &MultiReaderConn{ + Conn: session.conn, + reader: io.MultiReader(bytes.NewReader(b), bytes.NewReader(buf), session.conn), + } + + // 22 is the TLS handshake. + // https://tools.ietf.org/html/rfc5246#section-6.2.1 + if b[0] == 22 { + // TODO serve by generic host name maybe better? + tlsConn := tls.Server(mc, opt.CertConfig.NewTLSConfigForHost(session.request.URL.Host)) + + // Handshake with the local client + if err = tlsConn.Handshake(); err != nil { + handleError(opt, session, err) + break readLoop // close connection + } + + c = tlsConn + goto startOver // hijack and decrypt tls connection + } + + // maybe it's the others encrypted connection + in <- inbound.NewHTTPS(request, mc) + } + + // maybe it's a http connection + goto readLoop + } + + // hijack api + if getHostnameWithoutPort(session.request) == opt.ApiHost { + if err = handleApiRequest(session, opt); err != nil { + handleError(opt, session, err) + break readLoop + } + return + } + + prepareRequest(c, session.request) + + // hijack custom request and write back custom response if necessary + if opt.Handler != nil { + newReq, newRes := opt.Handler.HandleRequest(session) + if newReq != nil { + session.request = newReq + } + if newRes != nil { + session.response = newRes + + if err = writeResponse(session, false); err != nil { + handleError(opt, session, err) + break readLoop + } + return + } + } + + httpL.RemoveHopByHopHeaders(session.request.Header) + httpL.RemoveExtraHTTPHostPort(request) + + session.request.RequestURI = "" + + if session.request.URL.Scheme == "" || session.request.URL.Host == "" { + session.response = session.NewErrorResponse(errors.New("invalid URL")) + } else { + client = newClientBySourceAndUserAgentIfNil(client, session.request, source, in) + + // send the request to remote server + session.response, err = client.Do(session.request) + + if err != nil { + handleError(opt, session, err) + session.response = session.NewErrorResponse(err) + if errors.Is(err, ErrCertUnsupported) || strings.Contains(err.Error(), "x509: ") { + // TODO block unsupported host? + } + } + } + } + + if err = writeResponseWithHandler(session, opt); err != nil { + handleError(opt, session, err) + break readLoop // close connection + } + } + + _ = conn.Close() +} + +func writeResponseWithHandler(session *Session, opt *Option) error { + if opt.Handler != nil { + res := opt.Handler.HandleResponse(session) + + if res != nil { + body := res.Body + defer func(body io.ReadCloser) { + _ = body.Close() + }(body) + + session.response = res + } + } + + return writeResponse(session, true) +} + +func writeResponse(session *Session, keepAlive bool) error { + httpL.RemoveHopByHopHeaders(session.response.Header) + + if keepAlive { + session.response.Header.Set("Connection", "keep-alive") + session.response.Header.Set("Keep-Alive", "timeout=25") + } + + // session.response.Close = !keepAlive // let handler do it + + return session.response.Write(session.conn) +} + +func handleApiRequest(session *Session, opt *Option) error { + if opt.CertConfig != nil && strings.ToLower(session.request.URL.Path) == "/cert.crt" { + b := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: opt.CertConfig.GetCA().Raw, + }) + + session.response = session.NewResponse(http.StatusOK, bytes.NewReader(b)) + + defer func(body io.ReadCloser) { + _ = body.Close() + }(session.response.Body) + + session.response.Close = true + session.response.Header.Set("Content-Type", "application/x-x509-ca-cert") + session.response.ContentLength = int64(len(b)) + + return session.response.Write(session.conn) + } + + b := ` + + Clash ManInTheMiddle Proxy Services - 404 Not Found + + +

Not Found

+

The requested URL %s was not found on this server.

+ + +` + if opt.Handler != nil { + if opt.Handler.HandleApiRequest(session) { + return nil + } + } + + b = fmt.Sprintf(b, session.request.URL.Path) + + session.response = session.NewResponse(http.StatusNotFound, bytes.NewReader([]byte(b))) + + defer func(body io.ReadCloser) { + _ = body.Close() + }(session.response.Body) + + session.response.Close = true + session.response.Header.Set("Content-Type", "text/html;charset=utf-8") + session.response.ContentLength = int64(len(b)) + + return session.response.Write(session.conn) +} + +func handleError(opt *Option, session *Session, err error) { + if opt.Handler != nil { + opt.Handler.HandleError(session, err) + return + } + + // log.Errorln("[MITM] process mitm error: %v", err) +} + +func prepareRequest(conn net.Conn, request *http.Request) { + host := request.Header.Get("Host") + if host != "" { + request.Host = host + } + + if request.URL.Host == "" { + request.URL.Host = request.Host + } + + request.URL.Scheme = "http" + + if tlsConn, ok := conn.(*tls.Conn); ok { + cs := tlsConn.ConnectionState() + request.TLS = &cs + + request.URL.Scheme = "https" + } + + if request.Header.Get("Accept-Encoding") != "" { + request.Header.Set("Accept-Encoding", "gzip") + } +} + +func couldBeWithManInTheMiddleAttack(hostname string, opt *Option) bool { + if opt.CertConfig == nil { + return false + } + + if _, port, err := net.SplitHostPort(hostname); err == nil && (port == "443" || port == "8443") { + return true + } + + return false +} + +func getHostnameWithoutPort(req *http.Request) string { + host := req.Host + if host == "" { + host = req.URL.Host + } + + if pHost, _, err := net.SplitHostPort(host); err == nil { + host = pHost + } + + return host +} + +func parseSourceAddress(req *http.Request, c net.Conn, source net.Addr) net.Addr { + if source != nil { + return source + } + + sourceAddress := req.Header.Get("Origin-Request-Source-Address") + if sourceAddress == "" { + return c.RemoteAddr() + } + + req.Header.Del("Origin-Request-Source-Address") + + host, port, err := net.SplitHostPort(sourceAddress) + if err != nil { + return c.RemoteAddr() + } + + p, err := strconv.ParseUint(port, 10, 16) + if err != nil { + return c.RemoteAddr() + } + + if ip := net.ParseIP(host); ip != nil { + return &net.TCPAddr{ + IP: ip, + Port: int(p), + } + } + + return c.RemoteAddr() +} + +func newClientBySourceAndUserAgentIfNil(cli *http.Client, req *http.Request, source net.Addr, in chan<- C.ConnContext) *http.Client { + if cli != nil { + return cli + } + + return newClient(source, req.Header.Get("User-Agent"), in) +} diff --git a/listener/mitm/server.go b/listener/mitm/server.go new file mode 100644 index 00000000..d7699b81 --- /dev/null +++ b/listener/mitm/server.go @@ -0,0 +1,90 @@ +package mitm + +import ( + "crypto/tls" + "net" + "net/http" + "time" + + "github.com/Dreamacro/clash/common/cache" + "github.com/Dreamacro/clash/common/cert" + C "github.com/Dreamacro/clash/constant" +) + +type Handler interface { + HandleRequest(*Session) (*http.Request, *http.Response) // Session.Response maybe nil + HandleResponse(*Session) *http.Response + HandleApiRequest(*Session) bool + HandleError(*Session, error) // Session maybe nil +} + +type Option struct { + Addr string + ApiHost string + + TLSConfig *tls.Config + CertConfig *cert.Config + + Handler Handler +} + +type Listener struct { + *Option + + listener net.Listener + addr string + closed bool +} + +// RawAddress implements C.Listener +func (l *Listener) RawAddress() string { + return l.addr +} + +// Address implements C.Listener +func (l *Listener) Address() string { + return l.listener.Addr().String() +} + +// Close implements C.Listener +func (l *Listener) Close() error { + l.closed = true + return l.listener.Close() +} + +// New the MITM proxy actually is a type of HTTP proxy +func New(option *Option, in chan<- C.ConnContext) (*Listener, error) { + return NewWithAuthenticate(option, in, false) +} + +func NewWithAuthenticate(option *Option, in chan<- C.ConnContext, authenticate bool) (*Listener, error) { + l, err := net.Listen("tcp", option.Addr) + if err != nil { + return nil, err + } + + var c *cache.Cache[string, bool] + if authenticate { + c = cache.New[string, bool](time.Second * 30) + } + + hl := &Listener{ + listener: l, + addr: option.Addr, + Option: option, + } + go func() { + for { + conn, err1 := hl.listener.Accept() + if err1 != nil { + if hl.closed { + break + } + continue + } + go HandleConn(conn, option, in, c) + } + }() + + return hl, nil +} diff --git a/listener/mitm/session.go b/listener/mitm/session.go new file mode 100644 index 00000000..2572d879 --- /dev/null +++ b/listener/mitm/session.go @@ -0,0 +1,56 @@ +package mitm + +import ( + "fmt" + "io" + "net" + "net/http" + + C "github.com/Dreamacro/clash/constant" +) + +var serverName = fmt.Sprintf("Clash server (%s)", C.Version) + +type Session struct { + conn net.Conn + request *http.Request + response *http.Response + + props map[string]any +} + +func (s *Session) Request() *http.Request { + return s.request +} + +func (s *Session) Response() *http.Response { + return s.response +} + +func (s *Session) GetProperties(key string) (any, bool) { + v, ok := s.props[key] + return v, ok +} + +func (s *Session) SetProperties(key string, val any) { + s.props[key] = val +} + +func (s *Session) NewResponse(code int, body io.Reader) *http.Response { + res := NewResponse(code, body, s.request) + res.Header.Set("Server", serverName) + return res +} + +func (s *Session) NewErrorResponse(err error) *http.Response { + return NewErrorResponse(s.request, err) +} + +func NewSession(conn net.Conn, request *http.Request, response *http.Response) *Session { + return &Session{ + conn: conn, + request: request, + response: response, + props: map[string]any{}, + } +} diff --git a/listener/mitm/utils.go b/listener/mitm/utils.go new file mode 100644 index 00000000..7d681d42 --- /dev/null +++ b/listener/mitm/utils.go @@ -0,0 +1,100 @@ +package mitm + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "time" + + "golang.org/x/text/encoding/charmap" + "golang.org/x/text/transform" +) + +type MultiReaderConn struct { + net.Conn + reader io.Reader +} + +func (c *MultiReaderConn) Read(buf []byte) (int, error) { + return c.reader.Read(buf) +} + +func NewResponse(code int, body io.Reader, req *http.Request) *http.Response { + if body == nil { + body = &bytes.Buffer{} + } + + rc, ok := body.(io.ReadCloser) + if !ok { + rc = ioutil.NopCloser(body) + } + + res := &http.Response{ + StatusCode: code, + Status: fmt.Sprintf("%d %s", code, http.StatusText(code)), + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: http.Header{}, + Body: rc, + Request: req, + } + + if req != nil { + res.Close = req.Close + res.Proto = req.Proto + res.ProtoMajor = req.ProtoMajor + res.ProtoMinor = req.ProtoMinor + } + + return res +} + +func NewErrorResponse(req *http.Request, err error) *http.Response { + res := NewResponse(http.StatusBadGateway, nil, req) + res.Close = true + + date := res.Header.Get("Date") + if date == "" { + date = time.Now().Format(http.TimeFormat) + } + + w := fmt.Sprintf(`199 "clash" %q %q`, err.Error(), date) + res.Header.Add("Warning", w) + res.Header.Set("Server", serverName) + return res +} + +func ReadDecompressedBody(res *http.Response) ([]byte, error) { + rBody := res.Body + if res.Header.Get("Content-Encoding") == "gzip" { + gzReader, err := gzip.NewReader(rBody) + if err != nil { + return nil, err + } + rBody = gzReader + + defer func(gzReader *gzip.Reader) { + _ = gzReader.Close() + }(gzReader) + } + return ioutil.ReadAll(rBody) +} + +func DecodeLatin1(reader io.Reader) (string, error) { + r := transform.NewReader(reader, charmap.ISO8859_1.NewDecoder()) + b, err := ioutil.ReadAll(r) + if err != nil { + return "", err + } + + return string(b), nil +} + +func EncodeLatin1(str string) ([]byte, error) { + return charmap.ISO8859_1.NewEncoder().Bytes([]byte(str)) +} diff --git a/rewrite/base.go b/rewrite/base.go new file mode 100644 index 00000000..29ba0dc2 --- /dev/null +++ b/rewrite/base.go @@ -0,0 +1,72 @@ +package rewrites + +import ( + "bytes" + "io" + "io/ioutil" + + C "github.com/Dreamacro/clash/constant" +) + +var ( + EmptyDict = NewResponseBody([]byte("{}")) + EmptyArray = NewResponseBody([]byte("[]")) + OnePixelPNG = NewResponseBody([]byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x11, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x62, 0x62, 0x60, 0x60, 0x60, 0x00, 0x04, 0x00, 0x00, 0xff, 0xff, 0x00, 0x0f, 0x00, 0x03, 0xfe, 0x8f, 0xeb, 0xcf, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82}) +) + +type Body interface { + Body() io.ReadCloser + ContentLength() int64 +} + +type ResponseBody struct { + data []byte + length int64 +} + +func (r *ResponseBody) Body() io.ReadCloser { + return ioutil.NopCloser(bytes.NewReader(r.data)) +} + +func (r *ResponseBody) ContentLength() int64 { + return r.length +} + +func NewResponseBody(data []byte) *ResponseBody { + return &ResponseBody{ + data: data, + length: int64(len(data)), + } +} + +type RewriteRules struct { + request []C.Rewrite + response []C.Rewrite +} + +func (rr *RewriteRules) SearchInRequest(do func(C.Rewrite) bool) bool { + for _, v := range rr.request { + if do(v) { + return true + } + } + return false +} + +func (rr *RewriteRules) SearchInResponse(do func(C.Rewrite) bool) bool { + for _, v := range rr.response { + if do(v) { + return true + } + } + return false +} + +func NewRewriteRules(req []C.Rewrite, res []C.Rewrite) *RewriteRules { + return &RewriteRules{ + request: req, + response: res, + } +} + +var _ C.RewriteRule = (*RewriteRules)(nil) diff --git a/rewrite/handler.go b/rewrite/handler.go new file mode 100644 index 00000000..ddbafeb9 --- /dev/null +++ b/rewrite/handler.go @@ -0,0 +1,202 @@ +package rewrites + +import ( + "bufio" + "bytes" + "errors" + "io" + "io/ioutil" + "net/http" + "net/textproto" + "strconv" + "strings" + + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/listener/mitm" + "github.com/Dreamacro/clash/tunnel" +) + +var _ mitm.Handler = (*RewriteHandler)(nil) + +type RewriteHandler struct{} + +func (*RewriteHandler) HandleRequest(session *mitm.Session) (*http.Request, *http.Response) { + var ( + request = session.Request() + response *http.Response + ) + + rule, sub, found := matchRewriteRule(request.URL.String(), true) + if !found { + return nil, nil + } + + switch rule.RuleType() { + case C.MitmReject: + response = session.NewResponse(http.StatusNotFound, nil) + response.Header.Set("Content-Type", "text/html; charset=utf-8") + case C.MitmReject200: + response = session.NewResponse(http.StatusOK, nil) + response.Header.Set("Content-Type", "text/html; charset=utf-8") + case C.MitmRejectImg: + response = session.NewResponse(http.StatusOK, OnePixelPNG.Body()) + response.Header.Set("Content-Type", "image/png") + response.ContentLength = OnePixelPNG.ContentLength() + case C.MitmRejectDict: + response = session.NewResponse(http.StatusOK, EmptyDict.Body()) + response.Header.Set("Content-Type", "application/json; charset=utf-8") + response.ContentLength = EmptyDict.ContentLength() + case C.MitmRejectArray: + response = session.NewResponse(http.StatusOK, EmptyArray.Body()) + response.Header.Set("Content-Type", "application/json; charset=utf-8") + response.ContentLength = EmptyArray.ContentLength() + case C.Mitm302: + response = session.NewResponse(http.StatusFound, nil) + response.Header.Set("Location", rule.ReplaceURLPayload(sub)) + case C.Mitm307: + response = session.NewResponse(http.StatusTemporaryRedirect, nil) + response.Header.Set("Location", rule.ReplaceURLPayload(sub)) + case C.MitmRequestHeader: + if len(request.Header) == 0 { + return nil, nil + } + + rawHeader := &bytes.Buffer{} + oldHeader := request.Header + if err := oldHeader.Write(rawHeader); err != nil { + return nil, nil + } + + newRawHeader := rule.ReplaceSubPayload(rawHeader.String()) + tb := textproto.NewReader(bufio.NewReader(strings.NewReader(newRawHeader))) + newHeader, err := tb.ReadMIMEHeader() + if err != nil && !errors.Is(err, io.EOF) { + return nil, nil + } + request.Header = http.Header(newHeader) + case C.MitmRequestBody: + if !CanRewriteBody(request.ContentLength, request.Header.Get("Content-Type")) { + return nil, nil + } + + buf := make([]byte, request.ContentLength) + _, err := io.ReadFull(request.Body, buf) + if err != nil { + return nil, nil + } + + newBody := rule.ReplaceSubPayload(string(buf)) + request.Body = io.NopCloser(strings.NewReader(newBody)) + request.ContentLength = int64(len(newBody)) + default: + found = false + } + + if found { + if response != nil { + response.Close = true + } + return request, response + } + return nil, nil +} + +func (*RewriteHandler) HandleResponse(session *mitm.Session) *http.Response { + var ( + request = session.Request() + response = session.Response() + ) + + rule, _, found := matchRewriteRule(request.URL.String(), false) + found = found && rule.RuleRegx() != nil + if !found { + return nil + } + + switch rule.RuleType() { + case C.MitmResponseHeader: + if len(response.Header) == 0 { + return nil + } + + rawHeader := &bytes.Buffer{} + oldHeader := response.Header + if err := oldHeader.Write(rawHeader); err != nil { + return nil + } + + newRawHeader := rule.ReplaceSubPayload(rawHeader.String()) + tb := textproto.NewReader(bufio.NewReader(strings.NewReader(newRawHeader))) + newHeader, err := tb.ReadMIMEHeader() + if err != nil && !errors.Is(err, io.EOF) { + return nil + } + + response.Header = http.Header(newHeader) + response.Header.Set("Content-Length", strconv.FormatInt(response.ContentLength, 10)) + case C.MitmResponseBody: + if !CanRewriteBody(response.ContentLength, response.Header.Get("Content-Type")) { + return nil + } + + b, err := mitm.ReadDecompressedBody(response) + _ = response.Body.Close() + if err != nil { + return nil + } + + body, err := mitm.DecodeLatin1(bytes.NewReader(b)) + if err != nil { + return nil + } + + newBody := rule.ReplaceSubPayload(body) + + modifiedBody, err := mitm.EncodeLatin1(newBody) + if err != nil { + return nil + } + + response.Body = ioutil.NopCloser(bytes.NewReader(modifiedBody)) + response.Header.Del("Content-Encoding") + response.ContentLength = int64(len(modifiedBody)) + default: + found = false + } + + if found { + return response + } + return nil +} + +func (h *RewriteHandler) HandleApiRequest(*mitm.Session) bool { + return false +} + +// HandleError session maybe nil +func (h *RewriteHandler) HandleError(*mitm.Session, error) {} + +func matchRewriteRule(url string, isRequest bool) (rr C.Rewrite, sub []string, found bool) { + rewrites := tunnel.Rewrites() + if isRequest { + found = rewrites.SearchInRequest(func(r C.Rewrite) bool { + sub = r.URLRegx().FindStringSubmatch(url) + if len(sub) != 0 { + rr = r + return true + } + return false + }) + } else { + found = rewrites.SearchInResponse(func(r C.Rewrite) bool { + if r.URLRegx().FindString(url) != "" { + rr = r + return true + } + return false + }) + } + + return +} diff --git a/rewrite/parser.go b/rewrite/parser.go new file mode 100644 index 00000000..f97134d3 --- /dev/null +++ b/rewrite/parser.go @@ -0,0 +1,78 @@ +package rewrites + +import ( + "regexp" + "strings" + + C "github.com/Dreamacro/clash/constant" +) + +func ParseRewrite(line string) (C.Rewrite, error) { + url, others, found := strings.Cut(strings.TrimSpace(line), "url") + if !found { + return nil, errInvalid + } + + var ( + urlRegx *regexp.Regexp + ruleType *C.RewriteType + ruleRegx *regexp.Regexp + rulePayload string + + err error + ) + + urlRegx, err = regexp.Compile(strings.Trim(url, " ")) + if err != nil { + return nil, err + } + + others = strings.Trim(others, " ") + first := strings.Split(others, " ")[0] + for k, v := range C.RewriteTypeMapping { + if k == others { + ruleType = &v + break + } + + if k != first { + continue + } + + rs := trimArr(strings.Split(others, k)) + l := len(rs) + if l > 2 { + continue + } + + if l == 1 { + ruleType = &v + rulePayload = rs[0] + break + } else { + ruleRegx, err = regexp.Compile(rs[0]) + if err != nil { + return nil, err + } + + ruleType = &v + rulePayload = rs[1] + break + } + } + + if ruleType == nil { + return nil, errInvalid + } + + return NewRewriteRule(urlRegx, *ruleType, ruleRegx, rulePayload), nil +} + +func trimArr(arr []string) (r []string) { + for _, e := range arr { + if s := strings.Trim(e, " "); s != "" { + r = append(r, s) + } + } + return +} diff --git a/rewrite/parser_test.go b/rewrite/parser_test.go new file mode 100644 index 00000000..58d1149a --- /dev/null +++ b/rewrite/parser_test.go @@ -0,0 +1,56 @@ +package rewrites + +import ( + "bytes" + "fmt" + "image" + "image/color" + "image/draw" + "image/png" + "regexp" + "testing" + + "github.com/Dreamacro/clash/constant" + + "github.com/stretchr/testify/assert" +) + +func TestParseRewrite(t *testing.T) { + line0 := `^https?://example\.com/resource1/3/ url reject-dict` + line1 := `^https?://example\.com/(resource2)/ url 307 https://example.com/new-$1` + line2 := `^https?://example\.com/resource4/ url request-header (\r\n)User-Agent:.+(\r\n) request-header $1User-Agent: Fuck-Who$2` + line3 := `should be error` + + c0, err0 := ParseRewrite(line0) + c1, err1 := ParseRewrite(line1) + c2, err2 := ParseRewrite(line2) + _, err3 := ParseRewrite(line3) + + assert.NotNil(t, err3) + + assert.Nil(t, err0) + assert.Equal(t, c0.RuleType(), constant.MitmRejectDict) + + assert.Nil(t, err1) + assert.Equal(t, c1.RuleType(), constant.Mitm307) + assert.Equal(t, c1.URLRegx(), regexp.MustCompile(`^https?://example\.com/(resource2)/`)) + assert.Equal(t, c1.RulePayload(), "https://example.com/new-$1") + + assert.Nil(t, err2) + assert.Equal(t, c2.RuleType(), constant.MitmRequestHeader) + assert.Equal(t, c2.RuleRegx(), regexp.MustCompile(`(\r\n)User-Agent:.+(\r\n)`)) + assert.Equal(t, c2.RulePayload(), "$1User-Agent: Fuck-Who$2") +} + +func Test1PxPNG(t *testing.T) { + m := image.NewRGBA(image.Rect(0, 0, 1, 1)) + + draw.Draw(m, m.Bounds(), &image.Uniform{C: color.Transparent}, image.Point{}, draw.Src) + + buf := &bytes.Buffer{} + + assert.Nil(t, png.Encode(buf, m)) + + fmt.Printf("len: %d\n", buf.Len()) + fmt.Printf("% #x\n", buf.Bytes()) +} diff --git a/rewrite/rewrite.go b/rewrite/rewrite.go new file mode 100644 index 00000000..d88d4efe --- /dev/null +++ b/rewrite/rewrite.go @@ -0,0 +1,89 @@ +package rewrites + +import ( + "errors" + "regexp" + "strconv" + "strings" + + C "github.com/Dreamacro/clash/constant" + + "github.com/gofrs/uuid" +) + +var errInvalid = errors.New("invalid rewrite rule") + +type RewriteRule struct { + id string + urlRegx *regexp.Regexp + ruleType C.RewriteType + ruleRegx *regexp.Regexp + rulePayload string +} + +func (r *RewriteRule) ID() string { + return r.id +} + +func (r *RewriteRule) URLRegx() *regexp.Regexp { + return r.urlRegx +} + +func (r *RewriteRule) RuleType() C.RewriteType { + return r.ruleType +} + +func (r *RewriteRule) RuleRegx() *regexp.Regexp { + return r.ruleRegx +} + +func (r *RewriteRule) RulePayload() string { + return r.rulePayload +} + +func (r *RewriteRule) ReplaceURLPayload(matchSub []string) string { + url := r.rulePayload + + l := len(matchSub) + if l < 2 { + return url + } + + for i := 1; i < l; i++ { + url = strings.ReplaceAll(url, "$"+strconv.Itoa(i), matchSub[i]) + } + return url +} + +func (r *RewriteRule) ReplaceSubPayload(oldData string) string { + payload := r.rulePayload + if r.ruleRegx == nil { + return oldData + } + + sub := r.ruleRegx.FindStringSubmatch(oldData) + l := len(sub) + + if l == 0 { + return oldData + } + + for i := 1; i < l; i++ { + payload = strings.ReplaceAll(payload, "$"+strconv.Itoa(i), sub[i]) + } + + return strings.ReplaceAll(oldData, sub[0], payload) +} + +func NewRewriteRule(urlRegx *regexp.Regexp, ruleType C.RewriteType, ruleRegx *regexp.Regexp, rulePayload string) *RewriteRule { + id, _ := uuid.NewV4() + return &RewriteRule{ + id: id.String(), + urlRegx: urlRegx, + ruleType: ruleType, + ruleRegx: ruleRegx, + rulePayload: rulePayload, + } +} + +var _ C.Rewrite = (*RewriteRule)(nil) diff --git a/rewrite/util.go b/rewrite/util.go new file mode 100644 index 00000000..a12e4fa9 --- /dev/null +++ b/rewrite/util.go @@ -0,0 +1,28 @@ +package rewrites + +import ( + "strings" +) + +var allowContentType = []string{ + "text/", + "application/xhtml", + "application/xml", + "application/atom+xml", + "application/json", + "application/x-www-form-urlencoded", +} + +func CanRewriteBody(contentLength int64, contentType string) bool { + if contentLength <= 0 { + return false + } + + for _, v := range allowContentType { + if strings.HasPrefix(contentType, v) { + return true + } + } + + return false +} diff --git a/rule/parser.go b/rule/parser.go index 3374108e..5dc8dba4 100644 --- a/rule/parser.go +++ b/rule/parser.go @@ -37,6 +37,8 @@ func ParseRule(tp, payload, target string, params []string) (C.Rule, error) { parsed, parseErr = NewProcess(payload, target, true) case "PROCESS-PATH": parsed, parseErr = NewProcess(payload, target, false) + case "USER-AGENT": + parsed, parseErr = NewUserAgent(payload, target) case "MATCH": parsed = NewMatch(target) default: diff --git a/rule/user_gent.go b/rule/user_gent.go new file mode 100644 index 00000000..162188e5 --- /dev/null +++ b/rule/user_gent.go @@ -0,0 +1,52 @@ +package rules + +import ( + "strings" + + C "github.com/Dreamacro/clash/constant" +) + +type UserAgent struct { + *Base + ua string + adapter string +} + +func (d *UserAgent) RuleType() C.RuleType { + return C.UserAgent +} + +func (d *UserAgent) Match(metadata *C.Metadata) bool { + if metadata.Type != C.MITM || metadata.UserAgent == "" { + return false + } + + return strings.Contains(metadata.UserAgent, d.ua) +} + +func (d *UserAgent) Adapter() string { + return d.adapter +} + +func (d *UserAgent) Payload() string { + return d.ua +} + +func (d *UserAgent) ShouldResolveIP() bool { + return false +} + +func NewUserAgent(ua string, adapter string) (*UserAgent, error) { + ua = strings.Trim(ua, "*") + if ua == "" { + return nil, errPayload + } + + return &UserAgent{ + Base: &Base{}, + ua: ua, + adapter: adapter, + }, nil +} + +var _ C.Rule = (*UserAgent)(nil) diff --git a/test/go.mod b/test/go.mod index b4ec09f0..94f109dc 100644 --- a/test/go.mod +++ b/test/go.mod @@ -8,7 +8,7 @@ require ( github.com/docker/go-connections v0.4.0 github.com/miekg/dns v1.1.47 github.com/stretchr/testify v1.7.1 - golang.org/x/net v0.0.0-20220225172249-27dd8689420f + golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 ) replace github.com/Dreamacro/clash => ../ @@ -39,10 +39,10 @@ require ( github.com/xtls/go v0.0.0-20210920065950-d4af136d3672 // indirect go.etcd.io/bbolt v1.3.6 // indirect go.uber.org/atomic v1.9.0 // indirect - golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect - golang.org/x/mod v0.5.1 // indirect + golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 // indirect + golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 // indirect + golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab // indirect golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect golang.org/x/tools v0.1.9 // indirect diff --git a/test/go.sum b/test/go.sum index f1ffa840..991673db 100644 --- a/test/go.sum +++ b/test/go.sum @@ -913,8 +913,8 @@ golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWP golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38= -golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 h1:iU7T1X1J6yxDr0rda54sWGkHgOp5XJrqm79gcNlC2VM= +golang.org/x/crypto v0.0.0-20220408190544-5352b0902921/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -950,8 +950,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 h1:LQmS1nU0twXLA96Kt7U9qtHJEbBk3z6Q0V4UXjZkpr4= +golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1012,8 +1012,8 @@ golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 h1:EN5+DfgmRMvRUrMGERW2gQl3Vc+Z7ZMnI/xdEpPSf0c= +golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1144,8 +1144,8 @@ golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 h1:A9i04dxx7Cribqbs8jf3FQLogkL/CV2YN7hj9KWJCkc= -golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw= +golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/tunnel/statistic/tracker.go b/tunnel/statistic/tracker.go index 6fd8b3e7..1b24c107 100644 --- a/tunnel/statistic/tracker.go +++ b/tunnel/statistic/tracker.go @@ -80,8 +80,7 @@ func NewTCPTracker(conn C.Conn, manager *Manager, metadata *C.Metadata, rule C.R } manager.Join(t) - conn = NewSniffing(t, metadata) - return conn + return NewSniffing(t, metadata) } type udpTracker struct { diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go index b816871c..d2cb95be 100644 --- a/tunnel/tunnel.go +++ b/tunnel/tunnel.go @@ -26,6 +26,7 @@ var ( udpQueue = make(chan *inbound.PacketAdapter, 200) natTable = nat.New() rules []C.Rule + rewrites C.RewriteRule proxies = make(map[string]C.Proxy) providers map[string]provider.ProxyProvider configMux sync.RWMutex @@ -35,6 +36,9 @@ var ( // default timeout for UDP session udpTimeout = 60 * time.Second + + // MitmOutbound mitm proxy adapter + MitmOutbound C.ProxyAdapter ) func init() { @@ -91,6 +95,18 @@ func SetMode(m TunnelMode) { mode = m } +// Rewrites return all rewrites +func Rewrites() C.RewriteRule { + return rewrites +} + +// UpdateRewrites handle update rewrites +func UpdateRewrites(newRewrites C.RewriteRule) { + configMux.Lock() + rewrites = newRewrites + configMux.Unlock() +} + // processUDP starts a loop to handle udp packet func processUDP() { queue := udpQueue @@ -142,7 +158,7 @@ func preHandleMetadata(metadata *C.Metadata) error { metadata.DNSMode = C.DNSFakeIP } else if node := resolver.DefaultHosts.Search(host); node != nil { // redir-host should lookup the hosts - metadata.DstIP = node.Data.(net.IP) + metadata.DstIP = node.Data.AsSlice() } } else if resolver.IsFakeIP(metadata.DstIP) { return fmt.Errorf("fake DNS record %s missing", metadata.DstIP) @@ -281,14 +297,24 @@ func handleTCPConn(connCtx C.ConnContext) { return } + ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout) + defer cancel() + if MitmOutbound != nil && metadata.Type != C.MITM { + if remoteConn, err1 := MitmOutbound.DialContext(ctx, metadata); err1 == nil { + remoteConn = statistic.NewSniffing(remoteConn, metadata) + defer remoteConn.Close() + + handleSocket(connCtx, remoteConn) + return + } + } + proxy, rule, err := resolveMetadata(connCtx, metadata) if err != nil { log.Warnln("[Metadata] parse failed: %s", err.Error()) return } - ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout) - defer cancel() remoteConn, err := proxy.DialContext(ctx, metadata.Pure()) if err != nil { if rule == nil { @@ -326,8 +352,7 @@ func match(metadata *C.Metadata) (C.Proxy, C.Rule, error) { var resolved bool if node := resolver.DefaultHosts.Search(metadata.Host); node != nil { - ip := node.Data.(net.IP) - metadata.DstIP = ip + metadata.DstIP = node.Data.AsSlice() resolved = true }