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). Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki).
## Advanced usage for this branch ## 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 ### DNS configuration
Support resolve ip with a proxy tunnel. Support resolve ip with a proxy tunnel.
Support `geosite` with `fallback-filter`. 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 ```yaml
dns: dns:
enable: true enable: true
@ -85,6 +117,7 @@ tun:
``` ```
### Rules configuration ### Rules configuration
- Support rule `GEOSITE`. - Support rule `GEOSITE`.
- Support rule `USER-AGENT`.
- Support `multiport` condition for rule `SRC-PORT` and `DST-PORT`. - Support `multiport` condition for rule `SRC-PORT` and `DST-PORT`.
- Support `network` condition for all rules. - Support `network` condition for all rules.
- Support `process` condition for all rules. - Support `process` condition for all rules.
@ -105,6 +138,9 @@ rules:
# multiport condition for rules SRC-PORT and DST-PORT # multiport condition for rules SRC-PORT and DST-PORT
- DST-PORT,123/136/137-139,DIRECT,udp - 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 # rule GEOSITE
- GEOSITE,category-ads-all,REJECT - GEOSITE,category-ads-all,REJECT
- GEOSITE,icloud@cn,DIRECT - 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))) 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 { if err := req.Write(rw); err != nil {
return err 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) { 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) idx := strings.ToLower(asset + ":" + code)
if g.Has(idx) { if g.Has(idx) {
return g.Get(idx), nil 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) { 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) idx := strings.ToLower(asset + ":" + code)
if g.Has(idx) { if g.Has(idx) {
return g.Get(idx), nil return g.Get(idx), nil

View File

@ -26,7 +26,7 @@ func ReadFile(path string) ([]byte, error) {
} }
func ReadAsset(file 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) { func loadIP(filename, country string) ([]*router.CIDR, error) {

View File

@ -25,6 +25,7 @@ import (
"github.com/Dreamacro/clash/dns" "github.com/Dreamacro/clash/dns"
"github.com/Dreamacro/clash/listener/tun/ipstack/commons" "github.com/Dreamacro/clash/listener/tun/ipstack/commons"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
rewrites "github.com/Dreamacro/clash/rewrite"
R "github.com/Dreamacro/clash/rule" R "github.com/Dreamacro/clash/rule"
T "github.com/Dreamacro/clash/tunnel" T "github.com/Dreamacro/clash/tunnel"
@ -49,6 +50,7 @@ type Inbound struct {
RedirPort int `json:"redir-port"` RedirPort int `json:"redir-port"`
TProxyPort int `json:"tproxy-port"` TProxyPort int `json:"tproxy-port"`
MixedPort int `json:"mixed-port"` MixedPort int `json:"mixed-port"`
MitmPort int `json:"mitm-port"`
Authentication []string `json:"authentication"` Authentication []string `json:"authentication"`
AllowLan bool `json:"allow-lan"` AllowLan bool `json:"allow-lan"`
BindAddress string `json:"bind-address"` BindAddress string `json:"bind-address"`
@ -72,7 +74,7 @@ type DNS struct {
EnhancedMode C.DNSMode `yaml:"enhanced-mode"` EnhancedMode C.DNSMode `yaml:"enhanced-mode"`
DefaultNameserver []dns.NameServer `yaml:"default-nameserver"` DefaultNameserver []dns.NameServer `yaml:"default-nameserver"`
FakeIPRange *fakeip.Pool FakeIPRange *fakeip.Pool
Hosts *trie.DomainTrie Hosts *trie.DomainTrie[netip.Addr]
NameServerPolicy map[string]dns.NameServer NameServerPolicy map[string]dns.NameServer
ProxyServerNameserver []dns.NameServer ProxyServerNameserver []dns.NameServer
} }
@ -107,6 +109,12 @@ type IPTables struct {
InboundInterface string `yaml:"inbound-interface" json:"inbound-interface"` 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 // Experimental config
type Experimental struct{} type Experimental struct{}
@ -115,9 +123,10 @@ type Config struct {
General *General General *General
Tun *Tun Tun *Tun
IPTables *IPTables IPTables *IPTables
Mitm *Mitm
DNS *DNS DNS *DNS
Experimental *Experimental Experimental *Experimental
Hosts *trie.DomainTrie Hosts *trie.DomainTrie[netip.Addr]
Profile *Profile Profile *Profile
Rules []C.Rule Rules []C.Rule
Users []auth.AuthUser Users []auth.AuthUser
@ -157,12 +166,18 @@ type RawTun struct {
AutoRoute bool `yaml:"auto-route" json:"auto-route"` 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 { type RawConfig struct {
Port int `yaml:"port"` Port int `yaml:"port"`
SocksPort int `yaml:"socks-port"` SocksPort int `yaml:"socks-port"`
RedirPort int `yaml:"redir-port"` RedirPort int `yaml:"redir-port"`
TProxyPort int `yaml:"tproxy-port"` TProxyPort int `yaml:"tproxy-port"`
MixedPort int `yaml:"mixed-port"` MixedPort int `yaml:"mixed-port"`
MitmPort int `yaml:"mitm-port"`
Authentication []string `yaml:"authentication"` Authentication []string `yaml:"authentication"`
AllowLan bool `yaml:"allow-lan"` AllowLan bool `yaml:"allow-lan"`
BindAddress string `yaml:"bind-address"` BindAddress string `yaml:"bind-address"`
@ -180,6 +195,7 @@ type RawConfig struct {
DNS RawDNS `yaml:"dns"` DNS RawDNS `yaml:"dns"`
Tun RawTun `yaml:"tun"` Tun RawTun `yaml:"tun"`
IPTables IPTables `yaml:"iptables"` IPTables IPTables `yaml:"iptables"`
MITM RawMitm `yaml:"mitm"`
Experimental Experimental `yaml:"experimental"` Experimental Experimental `yaml:"experimental"`
Profile Profile `yaml:"profile"` Profile Profile `yaml:"profile"`
Proxy []map[string]any `yaml:"proxies"` Proxy []map[string]any `yaml:"proxies"`
@ -240,6 +256,10 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
"tls://223.5.5.5:853", "tls://223.5.5.5:853",
}, },
}, },
MITM: RawMitm{
Hosts: []string{},
Rules: []string{},
},
Profile: Profile{ Profile: Profile{
StoreSelected: true, StoreSelected: true,
}, },
@ -298,6 +318,12 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
} }
config.DNS = dnsCfg config.DNS = dnsCfg
mitm, err := parseMitm(rawCfg.MITM)
if err != nil {
return nil, err
}
config.Mitm = mitm
config.Users = parseAuthentication(rawCfg.Authentication) config.Users = parseAuthentication(rawCfg.Authentication)
return config, nil return config, nil
@ -322,6 +348,7 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
RedirPort: cfg.RedirPort, RedirPort: cfg.RedirPort,
TProxyPort: cfg.TProxyPort, TProxyPort: cfg.TProxyPort,
MixedPort: cfg.MixedPort, MixedPort: cfg.MixedPort,
MitmPort: cfg.MitmPort,
AllowLan: cfg.AllowLan, AllowLan: cfg.AllowLan,
BindAddress: cfg.BindAddress, BindAddress: cfg.BindAddress,
}, },
@ -501,24 +528,29 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, error) {
return rules, nil return rules, nil
} }
func parseHosts(cfg *RawConfig) (*trie.DomainTrie, error) { func parseHosts(cfg *RawConfig) (*trie.DomainTrie[netip.Addr], error) {
tree := trie.New() tree := trie.New[netip.Addr]()
// add default hosts // 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()) log.Errorln("insert localhost to host error: %s", err.Error())
} }
if len(cfg.Hosts) != 0 { if len(cfg.Hosts) != 0 {
for domain, ipStr := range cfg.Hosts { for domain, ipStr := range cfg.Hosts {
ip := net.ParseIP(ipStr) ip, err := netip.ParseAddr(ipStr)
if ip == nil { if err != nil {
return nil, fmt.Errorf("%s is not a valid IP", ipStr) return nil, fmt.Errorf("%s is not a valid IP", ipStr)
} }
_ = tree.Insert(domain, ip) _ = 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 return tree, nil
} }
@ -652,7 +684,7 @@ func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]*router.DomainM
return sites, nil 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 cfg := rawCfg.DNS
if cfg.Enable && len(cfg.NameServer) == 0 { if cfg.Enable && len(cfg.NameServer) == 0 {
return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty") 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 return nil, err
} }
var host *trie.DomainTrie var host *trie.DomainTrie[bool]
// fake ip skip host filter // fake ip skip host filter
if len(cfg.FakeIPFilter) != 0 { if len(cfg.FakeIPFilter) != 0 {
host = trie.New() host = trie.New[bool]()
for _, domain := range cfg.FakeIPFilter { for _, domain := range cfg.FakeIPFilter {
_ = host.Insert(domain, true) _ = 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 len(dnsCfg.Fallback) != 0 {
if host == nil { if host == nil {
host = trie.New() host = trie.New[bool]()
} }
for _, fb := range dnsCfg.Fallback { for _, fb := range dnsCfg.Fallback {
if net.ParseIP(fb.Addr) != nil { if net.ParseIP(fb.Addr) != nil {
@ -803,3 +835,38 @@ func parseTun(rawTun RawTun, general *General) (*Tun, error) {
AutoRoute: rawTun.AutoRoute, AutoRoute: rawTun.AutoRoute,
}, nil }, 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 ( const (
Direct AdapterType = iota Direct AdapterType = iota
Reject Reject
Mitm
Shadowsocks Shadowsocks
ShadowsocksR ShadowsocksR
@ -129,6 +130,8 @@ func (at AdapterType) String() string {
return "Direct" return "Direct"
case Reject: case Reject:
return "Reject" return "Reject"
case Mitm:
return "Mitm"
case Shadowsocks: case Shadowsocks:
return "Shadowsocks" return "Shadowsocks"

View File

@ -23,6 +23,7 @@ const (
REDIR REDIR
TPROXY TPROXY
TUN TUN
MITM
) )
type NetWork int type NetWork int
@ -58,6 +59,8 @@ func (t Type) String() string {
return "TProxy" return "TProxy"
case TUN: case TUN:
return "Tun" return "Tun"
case MITM:
return "Mitm"
default: default:
return "Unknown" return "Unknown"
} }
@ -80,6 +83,7 @@ type Metadata struct {
DNSMode DNSMode `json:"dnsMode"` DNSMode DNSMode `json:"dnsMode"`
Process string `json:"process"` Process string `json:"process"`
ProcessPath string `json:"processPath"` ProcessPath string `json:"processPath"`
UserAgent string `json:"userAgent"`
} }
func (m *Metadata) RemoteAddress() string { func (m *Metadata) RemoteAddress() string {

View File

@ -71,6 +71,10 @@ func (p *path) GeoSite() string {
return P.Join(p.homeDir, "geosite.dat") return P.Join(p.homeDir, "geosite.dat")
} }
func (p *path) GetAssetLocation(file string) string { func (p *path) RootCA() string {
return P.Join(p.homeDir, file) 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 DstPort
Process Process
ProcessPath ProcessPath
UserAgent
MATCH MATCH
) )
@ -42,6 +43,8 @@ func (rt RuleType) String() string {
return "Process" return "Process"
case ProcessPath: case ProcessPath:
return "ProcessPath" return "ProcessPath"
case UserAgent:
return "UserAgent"
case MATCH: case MATCH:
return "Match" return "Match"
default: default:

View File

@ -21,7 +21,7 @@ type (
middleware func(next handler) handler 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(next handler) handler {
return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) { return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) {
q := r.Question[0] q := r.Question[0]
@ -30,23 +30,28 @@ func withHosts(hosts *trie.DomainTrie[netip.Addr]) middleware {
return next(ctx, r) return next(ctx, r)
} }
record := hosts.Search(strings.TrimRight(q.Name, ".")) qName := strings.TrimRight(q.Name, ".")
record := hosts.Search(qName)
if record == nil { if record == nil {
return next(ctx, r) return next(ctx, r)
} }
ip := record.Data ip := record.Data
if mapping != nil {
mapping.SetWithExpire(ip.Unmap().String(), qName, time.Now().Add(time.Second*5))
}
msg := r.Copy() msg := r.Copy()
if ip.Is4() && q.Qtype == D.TypeA { if ip.Is4() && q.Qtype == D.TypeA {
rr := &D.A{} 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() rr.A = ip.AsSlice()
msg.Answer = []D.RR{rr} msg.Answer = []D.RR{rr}
} else if ip.Is6() && q.Qtype == D.TypeAAAA { } else if ip.Is6() && q.Qtype == D.TypeAAAA {
rr := &D.AAAA{} 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() rr.AAAA = ip.AsSlice()
msg.Answer = []D.RR{rr} msg.Answer = []D.RR{rr}
@ -177,7 +182,7 @@ func NewHandler(resolver *Resolver, mapper *ResolverEnhancer) handler {
middlewares := []middleware{} middlewares := []middleware{}
if resolver.hosts != nil { if resolver.hosts != nil {
middlewares = append(middlewares, withHosts(resolver.hosts)) middlewares = append(middlewares, withHosts(resolver.hosts, mapper.mapping))
} }
if mapper.mode == C.DNSFakeIP { if mapper.mode == C.DNSFakeIP {

10
go.mod
View File

@ -18,10 +18,11 @@ require (
go.etcd.io/bbolt v1.3.6 go.etcd.io/bbolt v1.3.6
go.uber.org/atomic v1.9.0 go.uber.org/atomic v1.9.0
go.uber.org/automaxprocs v1.4.0 go.uber.org/automaxprocs v1.4.0
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd golang.org/x/crypto v0.0.0-20220408190544-5352b0902921
golang.org/x/net v0.0.0-20220225172249-27dd8689420f golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c 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.org/x/time v0.0.0-20220224211638-0e9765cccd65
golang.zx2c4.com/wireguard v0.0.0-20220318042302-193cf8d6a5d6 golang.zx2c4.com/wireguard v0.0.0-20220318042302-193cf8d6a5d6
golang.zx2c4.com/wireguard/windows v0.5.4-0.20220317000008-6432784c2469 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/oschwald/maxminddb-golang v1.8.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect
golang.org/x/mod v0.5.1 // indirect golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 // indirect
golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab // indirect
golang.org/x/tools v0.1.9 // indirect golang.org/x/tools v0.1.9 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // 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-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-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-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38= golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 h1:iU7T1X1J6yxDr0rda54sWGkHgOp5XJrqm79gcNlC2VM=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 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.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 h1:LQmS1nU0twXLA96Kt7U9qtHJEbBk3z6Q0V4UXjZkpr4=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= 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-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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/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-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-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-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 h1:EN5+DfgmRMvRUrMGERW2gQl3Vc+Z7ZMnI/xdEpPSf0c=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 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-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 h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-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-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-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-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw=
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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/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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

View File

@ -3,12 +3,14 @@ package executor
import ( import (
"fmt" "fmt"
"net" "net"
"net/netip"
"os" "os"
"runtime" "runtime"
"strconv" "strconv"
"sync" "sync"
"github.com/Dreamacro/clash/adapter" "github.com/Dreamacro/clash/adapter"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapter/outboundgroup" "github.com/Dreamacro/clash/adapter/outboundgroup"
"github.com/Dreamacro/clash/component/auth" "github.com/Dreamacro/clash/component/auth"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
@ -71,12 +73,17 @@ func ApplyConfig(cfg *config.Config, force bool) {
mux.Lock() mux.Lock()
defer mux.Unlock() defer mux.Unlock()
if cfg.General.LogLevel == log.DEBUG {
log.SetLevel(log.DEBUG) log.SetLevel(log.DEBUG)
} else {
log.SetLevel(log.INFO)
}
updateUsers(cfg.Users) updateUsers(cfg.Users)
updateProxies(cfg.Proxies, cfg.Providers) updateProxies(cfg.Proxies, cfg.Providers)
updateRules(cfg.Rules) updateRules(cfg.Rules)
updateHosts(cfg.Hosts) updateHosts(cfg.Hosts)
updateMitm(cfg.Mitm)
updateProfile(cfg) updateProfile(cfg)
updateDNS(cfg.DNS, cfg.Tun) updateDNS(cfg.DNS, cfg.Tun)
updateGeneral(cfg.General, force) updateGeneral(cfg.General, force)
@ -101,6 +108,7 @@ func GetGeneral() *config.General {
RedirPort: ports.RedirPort, RedirPort: ports.RedirPort,
TProxyPort: ports.TProxyPort, TProxyPort: ports.TProxyPort,
MixedPort: ports.MixedPort, MixedPort: ports.MixedPort,
MitmPort: ports.MitmPort,
Authentication: authenticator, Authentication: authenticator,
AllowLan: P.AllowLan(), AllowLan: P.AllowLan(),
BindAddress: P.BindAddress(), 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 resolver.DefaultHosts = tree
} }
@ -225,6 +233,7 @@ func updateGeneral(general *config.General, force bool) {
P.ReCreateRedir(general.RedirPort, tcpIn, udpIn) P.ReCreateRedir(general.RedirPort, tcpIn, udpIn)
P.ReCreateTProxy(general.TProxyPort, tcpIn, udpIn) P.ReCreateTProxy(general.TProxyPort, tcpIn, udpIn)
P.ReCreateMixed(general.MixedPort, tcpIn, udpIn) P.ReCreateMixed(general.MixedPort, tcpIn, udpIn)
P.ReCreateMitm(general.MitmPort, tcpIn)
} }
func updateUsers(users []auth.AuthUser) { func updateUsers(users []auth.AuthUser) {
@ -330,6 +339,11 @@ func updateIPTables(cfg *config.Config) {
log.Infoln("[IPTABLES] Setting iptables completed") log.Infoln("[IPTABLES] Setting iptables completed")
} }
func updateMitm(mitm *config.Mitm) {
outbound.MiddlemanRewriteHosts = mitm.Hosts
tunnel.UpdateRewrites(mitm.Rules)
}
func Shutdown() { func Shutdown() {
P.Cleanup() P.Cleanup()
tproxy.CleanupTProxyIPTables() tproxy.CleanupTProxyIPTables()

View File

@ -30,6 +30,7 @@ type configSchema struct {
RedirPort *int `json:"redir-port"` RedirPort *int `json:"redir-port"`
TProxyPort *int `json:"tproxy-port"` TProxyPort *int `json:"tproxy-port"`
MixedPort *int `json:"mixed-port"` MixedPort *int `json:"mixed-port"`
MitmPort *int `json:"mitm-port"`
Tun *config.Tun `json:"tun"` Tun *config.Tun `json:"tun"`
AllowLan *bool `json:"allow-lan"` AllowLan *bool `json:"allow-lan"`
BindAddress *string `json:"bind-address"` 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.ReCreateRedir(pointerOrDefault(general.RedirPort, ports.RedirPort), tcpIn, udpIn)
P.ReCreateTProxy(pointerOrDefault(general.TProxyPort, ports.TProxyPort), tcpIn, udpIn) P.ReCreateTProxy(pointerOrDefault(general.TProxyPort, ports.TProxyPort), tcpIn, udpIn)
P.ReCreateMixed(pointerOrDefault(general.MixedPort, ports.MixedPort), tcpIn, udpIn) P.ReCreateMixed(pointerOrDefault(general.MixedPort, ports.MixedPort), tcpIn, udpIn)
P.ReCreateMitm(pointerOrDefault(general.MitmPort, ports.MitmPort), tcpIn)
if general.Mode != nil { if general.Mode != nil {
tunnel.SetMode(*general.Mode) 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 var resp *http.Response
if !trusted { if !trusted {
resp = authenticate(request, cache) resp = Authenticate(request, cache)
trusted = resp == nil trusted = resp == nil
} }
@ -66,19 +66,19 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.Cache[string,
request.RequestURI = "" request.RequestURI = ""
removeHopByHopHeaders(request.Header) RemoveHopByHopHeaders(request.Header)
removeExtraHTTPHostPort(request) RemoveExtraHTTPHostPort(request)
if request.URL.Scheme == "" || request.URL.Host == "" { if request.URL.Scheme == "" || request.URL.Host == "" {
resp = responseWith(request, http.StatusBadRequest) resp = ResponseWith(request, http.StatusBadRequest)
} else { } else {
resp, err = client.Do(request) resp, err = client.Do(request)
if err != nil { if err != nil {
resp = responseWith(request, http.StatusBadGateway) resp = ResponseWith(request, http.StatusBadGateway)
} }
} }
removeHopByHopHeaders(resp.Header) RemoveHopByHopHeaders(resp.Header)
} }
if keepAlive { if keepAlive {
@ -98,12 +98,12 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.Cache[string,
conn.Close() 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() authenticator := authStore.Authenticator()
if authenticator != nil { if authenticator != nil {
credential := parseBasicProxyAuthorization(request) credential := parseBasicProxyAuthorization(request)
if credential == "" { if credential == "" {
resp := responseWith(request, http.StatusProxyAuthRequired) resp := ResponseWith(request, http.StatusProxyAuthRequired)
resp.Header.Set("Proxy-Authenticate", "Basic") resp.Header.Set("Proxy-Authenticate", "Basic")
return resp return resp
} }
@ -117,14 +117,14 @@ func authenticate(request *http.Request, cache *cache.Cache[string, bool]) *http
if !authed { if !authed {
log.Infoln("Auth failed from %s", request.RemoteAddr) log.Infoln("Auth failed from %s", request.RemoteAddr)
return responseWith(request, http.StatusForbidden) return ResponseWith(request, http.StatusForbidden)
} }
} }
return nil return nil
} }
func responseWith(request *http.Request, statusCode int) *http.Response { func ResponseWith(request *http.Request, statusCode int) *http.Response {
return &http.Response{ return &http.Response{
StatusCode: statusCode, StatusCode: statusCode,
Status: http.StatusText(statusCode), Status: http.StatusText(statusCode),

View File

@ -8,8 +8,8 @@ import (
"strings" "strings"
) )
// removeHopByHopHeaders remove hop-by-hop header // RemoveHopByHopHeaders remove hop-by-hop header
func removeHopByHopHeaders(header http.Header) { func RemoveHopByHopHeaders(header http.Header) {
// Strip hop-by-hop header based on RFC: // Strip hop-by-hop header based on RFC:
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1 // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
// https://www.mnot.net/blog/2011/07/11/what_proxies_must_do // 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) // 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 host := req.Host
if host == "" { if host == "" {
host = req.URL.Host host = req.URL.Host

View File

@ -1,6 +1,9 @@
package proxy package proxy
import ( import (
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"fmt" "fmt"
"net" "net"
"os" "os"
@ -8,9 +11,12 @@ import (
"sync" "sync"
"github.com/Dreamacro/clash/adapter/inbound" "github.com/Dreamacro/clash/adapter/inbound"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/cert"
"github.com/Dreamacro/clash/config" "github.com/Dreamacro/clash/config"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/listener/http" "github.com/Dreamacro/clash/listener/http"
"github.com/Dreamacro/clash/listener/mitm"
"github.com/Dreamacro/clash/listener/mixed" "github.com/Dreamacro/clash/listener/mixed"
"github.com/Dreamacro/clash/listener/redir" "github.com/Dreamacro/clash/listener/redir"
"github.com/Dreamacro/clash/listener/socks" "github.com/Dreamacro/clash/listener/socks"
@ -18,6 +24,8 @@ import (
"github.com/Dreamacro/clash/listener/tun" "github.com/Dreamacro/clash/listener/tun"
"github.com/Dreamacro/clash/listener/tun/ipstack" "github.com/Dreamacro/clash/listener/tun/ipstack"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
rewrites "github.com/Dreamacro/clash/rewrite"
"github.com/Dreamacro/clash/tunnel"
) )
var ( var (
@ -34,6 +42,7 @@ var (
mixedListener *mixed.Listener mixedListener *mixed.Listener
mixedUDPLister *socks.UDPListener mixedUDPLister *socks.UDPListener
tunStackListener ipstack.Stack tunStackListener ipstack.Stack
mitmListener *mitm.Listener
// lock for recreate function // lock for recreate function
socksMux sync.Mutex socksMux sync.Mutex
@ -42,6 +51,7 @@ var (
tproxyMux sync.Mutex tproxyMux sync.Mutex
mixedMux sync.Mutex mixedMux sync.Mutex
tunMux sync.Mutex tunMux sync.Mutex
mitmMux sync.Mutex
) )
type Ports struct { type Ports struct {
@ -50,6 +60,7 @@ type Ports struct {
RedirPort int `json:"redir-port"` RedirPort int `json:"redir-port"`
TProxyPort int `json:"tproxy-port"` TProxyPort int `json:"tproxy-port"`
MixedPort int `json:"mixed-port"` MixedPort int `json:"mixed-port"`
MitmPort int `json:"mitm-port"`
} }
func AllowLan() bool { 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) 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 // GetPorts return the ports of proxy servers
func GetPorts() *Ports { func GetPorts() *Ports {
ports := &Ports{} ports := &Ports{}
@ -365,6 +455,12 @@ func GetPorts() *Ports {
ports.MixedPort = port ports.MixedPort = port
} }
if mitmListener != nil {
_, portStr, _ := net.SplitHostPort(mitmListener.Address())
port, _ := strconv.Atoi(portStr)
ports.MitmPort = port
}
return ports return ports
} }
@ -387,6 +483,19 @@ func genAddr(host string, port int, allowLan bool) string {
return fmt.Sprintf("127.0.0.1:%d", port) 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() { func Cleanup() {
if tunStackListener != nil { if tunStackListener != nil {
_ = tunStackListener.Close() _ = 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) parsed, parseErr = NewProcess(payload, target, true)
case "PROCESS-PATH": case "PROCESS-PATH":
parsed, parseErr = NewProcess(payload, target, false) parsed, parseErr = NewProcess(payload, target, false)
case "USER-AGENT":
parsed, parseErr = NewUserAgent(payload, target)
case "MATCH": case "MATCH":
parsed = NewMatch(target) parsed = NewMatch(target)
default: 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/docker/go-connections v0.4.0
github.com/miekg/dns v1.1.47 github.com/miekg/dns v1.1.47
github.com/stretchr/testify v1.7.1 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 => ../ replace github.com/Dreamacro/clash => ../
@ -39,10 +39,10 @@ require (
github.com/xtls/go v0.0.0-20210920065950-d4af136d3672 // indirect github.com/xtls/go v0.0.0-20210920065950-d4af136d3672 // indirect
go.etcd.io/bbolt v1.3.6 // indirect go.etcd.io/bbolt v1.3.6 // indirect
go.uber.org/atomic v1.9.0 // indirect go.uber.org/atomic v1.9.0 // indirect
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 // indirect
golang.org/x/mod v0.5.1 // 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/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/text v0.3.8-0.20220124021120-d1c84af989ab // indirect
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
golang.org/x/tools v0.1.9 // 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-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-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-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-20220408190544-5352b0902921 h1:iU7T1X1J6yxDr0rda54sWGkHgOp5XJrqm79gcNlC2VM=
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 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-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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 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.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/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.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 h1:LQmS1nU0twXLA96Kt7U9qtHJEbBk3z6Q0V4UXjZkpr4=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= 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-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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/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-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-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-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 h1:EN5+DfgmRMvRUrMGERW2gQl3Vc+Z7ZMnI/xdEpPSf0c=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/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-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-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-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-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw=
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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-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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/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) manager.Join(t)
conn = NewSniffing(t, metadata) return NewSniffing(t, metadata)
return conn
} }
type udpTracker struct { type udpTracker struct {

View File

@ -26,6 +26,7 @@ var (
udpQueue = make(chan *inbound.PacketAdapter, 200) udpQueue = make(chan *inbound.PacketAdapter, 200)
natTable = nat.New() natTable = nat.New()
rules []C.Rule rules []C.Rule
rewrites C.RewriteRule
proxies = make(map[string]C.Proxy) proxies = make(map[string]C.Proxy)
providers map[string]provider.ProxyProvider providers map[string]provider.ProxyProvider
configMux sync.RWMutex configMux sync.RWMutex
@ -35,6 +36,9 @@ var (
// default timeout for UDP session // default timeout for UDP session
udpTimeout = 60 * time.Second udpTimeout = 60 * time.Second
// MitmOutbound mitm proxy adapter
MitmOutbound C.ProxyAdapter
) )
func init() { func init() {
@ -91,6 +95,18 @@ func SetMode(m TunnelMode) {
mode = m 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 // processUDP starts a loop to handle udp packet
func processUDP() { func processUDP() {
queue := udpQueue queue := udpQueue
@ -142,7 +158,7 @@ func preHandleMetadata(metadata *C.Metadata) error {
metadata.DNSMode = C.DNSFakeIP metadata.DNSMode = C.DNSFakeIP
} else if node := resolver.DefaultHosts.Search(host); node != nil { } else if node := resolver.DefaultHosts.Search(host); node != nil {
// redir-host should lookup the hosts // redir-host should lookup the hosts
metadata.DstIP = node.Data.(net.IP) metadata.DstIP = node.Data.AsSlice()
} }
} else if resolver.IsFakeIP(metadata.DstIP) { } else if resolver.IsFakeIP(metadata.DstIP) {
return fmt.Errorf("fake DNS record %s missing", metadata.DstIP) return fmt.Errorf("fake DNS record %s missing", metadata.DstIP)
@ -281,14 +297,24 @@ func handleTCPConn(connCtx C.ConnContext) {
return 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) proxy, rule, err := resolveMetadata(connCtx, metadata)
if err != nil { if err != nil {
log.Warnln("[Metadata] parse failed: %s", err.Error()) log.Warnln("[Metadata] parse failed: %s", err.Error())
return return
} }
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
defer cancel()
remoteConn, err := proxy.DialContext(ctx, metadata.Pure()) remoteConn, err := proxy.DialContext(ctx, metadata.Pure())
if err != nil { if err != nil {
if rule == nil { if rule == nil {
@ -326,8 +352,7 @@ func match(metadata *C.Metadata) (C.Proxy, C.Rule, error) {
var resolved bool var resolved bool
if node := resolver.DefaultHosts.Search(metadata.Host); node != nil { if node := resolver.DefaultHosts.Search(metadata.Host); node != nil {
ip := node.Data.(net.IP) metadata.DstIP = node.Data.AsSlice()
metadata.DstIP = ip
resolved = true resolved = true
} }