Feature: MITM rewrite

This commit is contained in:
yaling888 2022-04-10 03:59:27 +08:00
parent 5a27ebd1b3
commit f036e06f6f
40 changed files with 2144 additions and 71 deletions

View File

@ -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.
@ -105,6 +138,9 @@ 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

22
adapter/inbound/mitm.go Normal file
View File

@ -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)
}

View File

@ -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
}

68
adapter/outbound/mitm.go Normal file
View File

@ -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,
},
}
}

282
common/cert/cert.go Normal file
View File

@ -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(&currentSerialNumber, 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(&currentSerialNumber, 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
}

76
common/cert/cert_test.go Normal file
View File

@ -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)
}

32
common/cert/storage.go Normal file
View File

@ -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),
}
}

View File

@ -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

View File

@ -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) {

View File

@ -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
}

View File

@ -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"

View File

@ -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 {

View File

@ -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")
}

82
constant/rewrite.go Normal file
View File

@ -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
}

View File

@ -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:

View File

@ -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 {

10
go.mod
View File

@ -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

16
go.sum
View File

@ -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=

View File

@ -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()

View File

@ -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)

View File

@ -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),

View File

@ -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

View File

@ -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()

54
listener/mitm/client.go Normal file
View File

@ -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
},
}
}

357
listener/mitm/proxy.go Normal file
View File

@ -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 := `<!DOCTYPE HTML PUBLIC "-
<html>
<head>
<title>Clash ManInTheMiddle Proxy Services - 404 Not Found</title>
</head>
<body>
<h1>Not Found</h1>
<p>The requested URL %s was not found on this server.</p>
</body>
</html>
`
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)
}

90
listener/mitm/server.go Normal file
View File

@ -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
}

56
listener/mitm/session.go Normal file
View File

@ -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{},
}
}

100
listener/mitm/utils.go Normal file
View File

@ -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))
}

72
rewrite/base.go Normal file
View File

@ -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)

202
rewrite/handler.go Normal file
View File

@ -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
}

78
rewrite/parser.go Normal file
View File

@ -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
}

56
rewrite/parser_test.go Normal file
View File

@ -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())
}

89
rewrite/rewrite.go Normal file
View File

@ -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)

28
rewrite/util.go Normal file
View File

@ -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
}

View File

@ -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:

52
rule/user_gent.go Normal file
View File

@ -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)

View File

@ -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

View File

@ -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=

View File

@ -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 {

View File

@ -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
}