Feature: MITM rewrite
This commit is contained in:
parent
5a27ebd1b3
commit
f036e06f6f
38
README.md
38
README.md
@ -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
22
adapter/inbound/mitm.go
Normal 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)
|
||||||
|
}
|
@ -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
68
adapter/outbound/mitm.go
Normal 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
282
common/cert/cert.go
Normal 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(¤tSerialNumber, 1)
|
||||||
|
|
||||||
|
tmpl := &x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(serial),
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: name,
|
||||||
|
Organization: []string{organization},
|
||||||
|
},
|
||||||
|
SubjectKeyId: keyID,
|
||||||
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
NotBefore: time.Now().Add(-validity),
|
||||||
|
NotAfter: time.Now().Add(validity),
|
||||||
|
DNSNames: []string{name},
|
||||||
|
IsCA: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, pub, privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
x509c, err := x509.ParseCertificate(raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return x509c, privateKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfig(ca *x509.Certificate, caPrivateKey *rsa.PrivateKey, storage CertsStorage) (*Config, error) {
|
||||||
|
roots := x509.NewCertPool()
|
||||||
|
roots.AddCert(ca)
|
||||||
|
|
||||||
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pub := privateKey.Public()
|
||||||
|
|
||||||
|
pkixPub, err := x509.MarshalPKIXPublicKey(pub)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
h := sha1.New()
|
||||||
|
_, err = h.Write(pkixPub)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
keyID := h.Sum(nil)
|
||||||
|
|
||||||
|
if storage == nil {
|
||||||
|
storage = &CertsCache{certsCache: make(map[string]*tls.Certificate)}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Config{
|
||||||
|
ca: ca,
|
||||||
|
caPrivateKey: caPrivateKey,
|
||||||
|
privateKey: privateKey,
|
||||||
|
keyID: keyID,
|
||||||
|
validity: time.Hour,
|
||||||
|
organization: "Clash",
|
||||||
|
certsStorage: storage,
|
||||||
|
roots: roots,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetCA() *x509.Certificate {
|
||||||
|
return c.ca
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) SetOrganization(organization string) {
|
||||||
|
c.organization = organization
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) SetValidity(validity time.Duration) {
|
||||||
|
c.validity = validity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) NewTLSConfigForHost(hostname string) *tls.Config {
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
host := clientHello.ServerName
|
||||||
|
if host == "" {
|
||||||
|
host = hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.GetOrCreateCert(host)
|
||||||
|
},
|
||||||
|
NextProtos: []string{"http/1.1"},
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConfig.InsecureSkipVerify = true
|
||||||
|
|
||||||
|
return tlsConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetOrCreateCert(hostname string, ips ...net.IP) (*tls.Certificate, error) {
|
||||||
|
host, _, err := net.SplitHostPort(hostname)
|
||||||
|
if err == nil {
|
||||||
|
hostname = host
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsCertificate, ok := c.certsStorage.Get(hostname)
|
||||||
|
if ok {
|
||||||
|
if _, err = tlsCertificate.Leaf.Verify(x509.VerifyOptions{
|
||||||
|
DNSName: hostname,
|
||||||
|
Roots: c.roots,
|
||||||
|
}); err == nil {
|
||||||
|
return tlsCertificate, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serial := atomic.AddInt64(¤tSerialNumber, 1)
|
||||||
|
|
||||||
|
tmpl := &x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(serial),
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: hostname,
|
||||||
|
Organization: []string{c.organization},
|
||||||
|
},
|
||||||
|
SubjectKeyId: c.keyID,
|
||||||
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
NotBefore: time.Now().Add(-c.validity),
|
||||||
|
NotAfter: time.Now().Add(c.validity),
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip := net.ParseIP(hostname); ip != nil {
|
||||||
|
ips = append(ips, ip)
|
||||||
|
} else {
|
||||||
|
tmpl.DNSNames = []string{hostname}
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl.IPAddresses = ips
|
||||||
|
|
||||||
|
raw, err := x509.CreateCertificate(rand.Reader, tmpl, c.ca, c.privateKey.Public(), c.caPrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
x509c, err := x509.ParseCertificate(raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsCertificate = &tls.Certificate{
|
||||||
|
Certificate: [][]byte{raw, c.ca.Raw},
|
||||||
|
PrivateKey: c.privateKey,
|
||||||
|
Leaf: x509c,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.certsStorage.Set(hostname, tlsCertificate)
|
||||||
|
return tlsCertificate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateAndSave generate CA private key and CA certificate and dump them to file
|
||||||
|
func GenerateAndSave(caPath string, caKeyPath string) error {
|
||||||
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := &x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(time.Now().Unix()),
|
||||||
|
Subject: pkix.Name{
|
||||||
|
Country: []string{"US"},
|
||||||
|
CommonName: "Clash Root CA",
|
||||||
|
Organization: []string{"Clash Trust Services"},
|
||||||
|
},
|
||||||
|
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
|
NotBefore: time.Now().Add(-(time.Hour * 24 * 60)),
|
||||||
|
NotAfter: time.Now().Add(time.Hour * 24 * 365 * 25),
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
IsCA: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
caRaw, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, privateKey.Public(), privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
caOut, err := os.OpenFile(caPath, os.O_CREATE|os.O_WRONLY, 0o600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func(caOut *os.File) {
|
||||||
|
_ = caOut.Close()
|
||||||
|
}(caOut)
|
||||||
|
|
||||||
|
if err = pem.Encode(caOut, &pem.Block{Type: "CERTIFICATE", Bytes: caRaw}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
caKeyOut, err := os.OpenFile(caKeyPath, os.O_CREATE|os.O_WRONLY, 0o600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func(caKeyOut *os.File) {
|
||||||
|
_ = caKeyOut.Close()
|
||||||
|
}(caKeyOut)
|
||||||
|
|
||||||
|
if err = pem.Encode(caKeyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
76
common/cert/cert_test.go
Normal file
76
common/cert/cert_test.go
Normal 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
32
common/cert/storage.go
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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 {
|
||||||
|
@ -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
82
constant/rewrite.go
Normal 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
|
||||||
|
}
|
@ -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:
|
||||||
|
@ -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
10
go.mod
@ -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
16
go.sum
@ -81,11 +81,11 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-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=
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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),
|
||||||
|
@ -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
|
||||||
|
@ -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
54
listener/mitm/client.go
Normal 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
357
listener/mitm/proxy.go
Normal 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
90
listener/mitm/server.go
Normal 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
56
listener/mitm/session.go
Normal 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
100
listener/mitm/utils.go
Normal 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
72
rewrite/base.go
Normal 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
202
rewrite/handler.go
Normal 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
78
rewrite/parser.go
Normal 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
56
rewrite/parser_test.go
Normal 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
89
rewrite/rewrite.go
Normal 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
28
rewrite/util.go
Normal 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
|
||||||
|
}
|
@ -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
52
rule/user_gent.go
Normal 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)
|
@ -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
|
||||||
|
16
test/go.sum
16
test/go.sum
@ -913,8 +913,8 @@ golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWP
|
|||||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/crypto v0.0.0-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=
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user