mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2024-12-22 23:57:26 +08:00
feature: MITM
This commit is contained in:
parent
d6b80acfbc
commit
2092a481b3
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); err == nil {
|
||||||
|
metadata.SrcIP = ip
|
||||||
|
metadata.SrcPort = port
|
||||||
|
}
|
||||||
|
return context.NewConnContext(conn, metadata)
|
||||||
|
}
|
@ -113,6 +113,10 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
|
|||||||
tempHeaders["Proxy-Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
|
tempHeaders["Proxy-Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if metadata.Type == C.MITM {
|
||||||
|
tempHeaders["Origin-Request-Source-Address"] = metadata.SourceAddress()
|
||||||
|
}
|
||||||
|
|
||||||
for key, value := range tempHeaders {
|
for key, value := range tempHeaders {
|
||||||
HeaderString += key + ": " + value + "\r\n"
|
HeaderString += key + ": " + value + "\r\n"
|
||||||
}
|
}
|
||||||
|
50
adapter/outbound/mitm.go
Normal file
50
adapter/outbound/mitm.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Mitm struct {
|
||||||
|
*Base
|
||||||
|
serverAddr *net.TCPAddr
|
||||||
|
httpProxyClient *Http
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialContext implements C.ProxyAdapter
|
||||||
|
func (m *Mitm) DialContext(ctx context.Context, metadata *C.Metadata, _ ...dialer.Option) (C.Conn, error) {
|
||||||
|
c, err := net.DialTCP("tcp", nil, m.serverAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = c.SetKeepAlive(true)
|
||||||
|
_ = c.SetKeepAlivePeriod(60 * time.Second)
|
||||||
|
|
||||||
|
metadata.Type = C.MITM
|
||||||
|
|
||||||
|
hc, err := m.httpProxyClient.StreamConnContext(ctx, c, metadata)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewConn(hc, m), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMitm(serverAddr string) *Mitm {
|
||||||
|
tcpAddr, _ := net.ResolveTCPAddr("tcp", serverAddr)
|
||||||
|
http, _ := NewHttp(HttpOption{})
|
||||||
|
return &Mitm{
|
||||||
|
Base: &Base{
|
||||||
|
name: "Mitm",
|
||||||
|
tp: C.Mitm,
|
||||||
|
},
|
||||||
|
serverAddr: tcpAddr,
|
||||||
|
httpProxyClient: http,
|
||||||
|
}
|
||||||
|
}
|
303
common/cert/cert.go
Normal file
303
common/cert/cert.go
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
package cert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/pem"
|
||||||
|
"math/big"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) (*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)
|
||||||
|
|
||||||
|
return &Config{
|
||||||
|
ca: ca,
|
||||||
|
caPrivateKey: caPrivateKey,
|
||||||
|
privateKey: privateKey,
|
||||||
|
keyID: keyID,
|
||||||
|
validity: time.Hour,
|
||||||
|
organization: "Clash",
|
||||||
|
certsStorage: NewDomainTrieCertsStorage(),
|
||||||
|
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) {
|
||||||
|
var leaf *x509.Certificate
|
||||||
|
tlsCertificate, ok := c.certsStorage.Get(hostname)
|
||||||
|
if ok {
|
||||||
|
leaf = tlsCertificate.Leaf
|
||||||
|
if _, err := leaf.Verify(x509.VerifyOptions{
|
||||||
|
DNSName: hostname,
|
||||||
|
Roots: c.roots,
|
||||||
|
}); err == nil {
|
||||||
|
return tlsCertificate, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
key = hostname
|
||||||
|
topHost = hostname
|
||||||
|
wildcardHost = "*." + hostname
|
||||||
|
dnsNames []string
|
||||||
|
)
|
||||||
|
|
||||||
|
if ip := net.ParseIP(hostname); ip != nil {
|
||||||
|
ips = append(ips, ip)
|
||||||
|
} else {
|
||||||
|
parts := strings.Split(hostname, ".")
|
||||||
|
l := len(parts)
|
||||||
|
|
||||||
|
if leaf != nil {
|
||||||
|
dnsNames = append(dnsNames, leaf.DNSNames...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if l > 2 {
|
||||||
|
topIndex := l - 2
|
||||||
|
topHost = strings.Join(parts[topIndex:], ".")
|
||||||
|
|
||||||
|
for i := topIndex; i > 0; i-- {
|
||||||
|
wildcardHost = "*." + strings.Join(parts[i:], ".")
|
||||||
|
|
||||||
|
if i == topIndex && (len(dnsNames) == 0 || dnsNames[0] != topHost) {
|
||||||
|
dnsNames = append(dnsNames, topHost, wildcardHost)
|
||||||
|
} else if !hasDnsNames(dnsNames, wildcardHost) {
|
||||||
|
dnsNames = append(dnsNames, wildcardHost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dnsNames = append(dnsNames, topHost, wildcardHost)
|
||||||
|
}
|
||||||
|
|
||||||
|
key = "+." + topHost
|
||||||
|
}
|
||||||
|
|
||||||
|
serial := atomic.AddInt64(¤tSerialNumber, 1)
|
||||||
|
|
||||||
|
tmpl := &x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(serial),
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: topHost,
|
||||||
|
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),
|
||||||
|
DNSNames: dnsNames,
|
||||||
|
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(key, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasDnsNames(dnsNames []string, hostname string) bool {
|
||||||
|
for _, name := range dnsNames {
|
||||||
|
if name == hostname {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
32
common/cert/storage.go
Normal file
32
common/cert/storage.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package cert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/trie"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DomainTrieCertsStorage cache wildcard certificates
|
||||||
|
type DomainTrieCertsStorage struct {
|
||||||
|
certsCache *trie.DomainTrie[*tls.Certificate]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get gets the certificate from the storage
|
||||||
|
func (c *DomainTrieCertsStorage) Get(key string) (*tls.Certificate, bool) {
|
||||||
|
ca := c.certsCache.Search(key)
|
||||||
|
if ca == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return ca.Data(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set saves the certificate to the storage
|
||||||
|
func (c *DomainTrieCertsStorage) Set(key string, cert *tls.Certificate) {
|
||||||
|
_ = c.certsCache.Insert(key, cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDomainTrieCertsStorage() *DomainTrieCertsStorage {
|
||||||
|
return &DomainTrieCertsStorage{
|
||||||
|
certsCache: trie.New[*tls.Certificate](),
|
||||||
|
}
|
||||||
|
}
|
@ -32,7 +32,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
|
||||||
@ -97,7 +97,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(geoipBytes []byte, country string) ([]*router.CIDR, error) {
|
func loadIP(geoipBytes []byte, country string) ([]*router.CIDR, error) {
|
||||||
|
@ -35,6 +35,7 @@ import (
|
|||||||
L "github.com/Dreamacro/clash/listener"
|
L "github.com/Dreamacro/clash/listener"
|
||||||
LC "github.com/Dreamacro/clash/listener/config"
|
LC "github.com/Dreamacro/clash/listener/config"
|
||||||
"github.com/Dreamacro/clash/log"
|
"github.com/Dreamacro/clash/log"
|
||||||
|
rewrites "github.com/Dreamacro/clash/rewrite"
|
||||||
R "github.com/Dreamacro/clash/rules"
|
R "github.com/Dreamacro/clash/rules"
|
||||||
RP "github.com/Dreamacro/clash/rules/provider"
|
RP "github.com/Dreamacro/clash/rules/provider"
|
||||||
T "github.com/Dreamacro/clash/tunnel"
|
T "github.com/Dreamacro/clash/tunnel"
|
||||||
@ -79,6 +80,7 @@ type Inbound struct {
|
|||||||
BindAddress string `json:"bind-address"`
|
BindAddress string `json:"bind-address"`
|
||||||
InboundTfo bool `json:"inbound-tfo"`
|
InboundTfo bool `json:"inbound-tfo"`
|
||||||
InboundMPTCP bool `json:"inbound-mptcp"`
|
InboundMPTCP bool `json:"inbound-mptcp"`
|
||||||
|
MitmPort int `json:"mitm-port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Controller config
|
// Controller config
|
||||||
@ -152,6 +154,12 @@ type Sniffer struct {
|
|||||||
ParsePureIp bool
|
ParsePureIp bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mitm config
|
||||||
|
type Mitm struct {
|
||||||
|
Port int `yaml:"port" json:"port"`
|
||||||
|
Rules C.RewriteRule `yaml:"rules" json:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
// Experimental config
|
// Experimental config
|
||||||
type Experimental struct {
|
type Experimental struct {
|
||||||
Fingerprints []string `yaml:"fingerprints"`
|
Fingerprints []string `yaml:"fingerprints"`
|
||||||
@ -161,6 +169,7 @@ type Experimental struct {
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
General *General
|
General *General
|
||||||
IPTables *IPTables
|
IPTables *IPTables
|
||||||
|
Mitm *Mitm
|
||||||
NTP *NTP
|
NTP *NTP
|
||||||
DNS *DNS
|
DNS *DNS
|
||||||
Experimental *Experimental
|
Experimental *Experimental
|
||||||
@ -253,12 +262,18 @@ type RawTuicServer struct {
|
|||||||
CWND int `yaml:"cwnd" json:"cwnd,omitempty"`
|
CWND int `yaml:"cwnd" json:"cwnd,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RawMitm struct {
|
||||||
|
Port int `yaml:"port" json:"port"`
|
||||||
|
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"`
|
||||||
ShadowSocksConfig string `yaml:"ss-config"`
|
ShadowSocksConfig string `yaml:"ss-config"`
|
||||||
VmessConfig string `yaml:"vmess-config"`
|
VmessConfig string `yaml:"vmess-config"`
|
||||||
InboundTfo bool `yaml:"inbound-tfo"`
|
InboundTfo bool `yaml:"inbound-tfo"`
|
||||||
@ -294,6 +309,7 @@ type RawConfig struct {
|
|||||||
TuicServer RawTuicServer `yaml:"tuic-server"`
|
TuicServer RawTuicServer `yaml:"tuic-server"`
|
||||||
EBpf EBpf `yaml:"ebpf"`
|
EBpf EBpf `yaml:"ebpf"`
|
||||||
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"`
|
||||||
GeoXUrl GeoXUrl `yaml:"geox-url"`
|
GeoXUrl GeoXUrl `yaml:"geox-url"`
|
||||||
@ -438,6 +454,10 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
|
|||||||
ParsePureIp: true,
|
ParsePureIp: true,
|
||||||
OverrideDest: true,
|
OverrideDest: true,
|
||||||
},
|
},
|
||||||
|
MITM: RawMitm{
|
||||||
|
Port: 0,
|
||||||
|
Rules: []string{},
|
||||||
|
},
|
||||||
Profile: Profile{
|
Profile: Profile{
|
||||||
StoreSelected: true,
|
StoreSelected: true,
|
||||||
},
|
},
|
||||||
@ -532,6 +552,12 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mitm, err := parseMitm(rawCfg.MITM)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
config.Mitm = mitm
|
||||||
|
|
||||||
config.Users = parseAuthentication(rawCfg.Authentication)
|
config.Users = parseAuthentication(rawCfg.Authentication)
|
||||||
|
|
||||||
config.Tunnels = rawCfg.Tunnels
|
config.Tunnels = rawCfg.Tunnels
|
||||||
@ -582,6 +608,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,
|
||||||
ShadowSocksConfig: cfg.ShadowSocksConfig,
|
ShadowSocksConfig: cfg.ShadowSocksConfig,
|
||||||
VmessConfig: cfg.VmessConfig,
|
VmessConfig: cfg.VmessConfig,
|
||||||
AllowLan: cfg.AllowLan,
|
AllowLan: cfg.AllowLan,
|
||||||
@ -629,6 +656,11 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
|
|||||||
proxies["PASS"] = adapter.NewProxy(outbound.NewPass())
|
proxies["PASS"] = adapter.NewProxy(outbound.NewPass())
|
||||||
proxyList = append(proxyList, "DIRECT", "REJECT")
|
proxyList = append(proxyList, "DIRECT", "REJECT")
|
||||||
|
|
||||||
|
if cfg.MITM.Port != 0 {
|
||||||
|
proxies["MITM"] = adapter.NewProxy(outbound.NewMitm(fmt.Sprintf("127.0.0.1:%d", cfg.MITM.Port)))
|
||||||
|
proxyList = append(proxyList, "MITM")
|
||||||
|
}
|
||||||
|
|
||||||
// parse proxy
|
// parse proxy
|
||||||
for idx, mapping := range proxiesConfig {
|
for idx, mapping := range proxiesConfig {
|
||||||
proxy, err := adapter.ParseProxy(mapping)
|
proxy, err := adapter.ParseProxy(mapping)
|
||||||
@ -909,6 +941,14 @@ func parseHosts(cfg *RawConfig) (*trie.DomainTrie[resolver.HostValue], error) {
|
|||||||
_ = tree.Insert(domain, value)
|
_ = tree.Insert(domain, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.MITM.Port != 0 {
|
||||||
|
value, _ := resolver.NewHostValue("8.8.9.9")
|
||||||
|
if err := tree.Insert("mitm.clash", value); err != nil {
|
||||||
|
log.Errorln("insert mitm.clash to host error: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tree.Optimize()
|
tree.Optimize()
|
||||||
|
|
||||||
return tree, nil
|
return tree, nil
|
||||||
@ -1457,3 +1497,28 @@ func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) {
|
|||||||
|
|
||||||
return sniffer, nil
|
return sniffer, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Mitm{
|
||||||
|
Port: rawMitm.Port,
|
||||||
|
Rules: rewrites.NewRewriteRules(req, res),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
@ -19,6 +19,7 @@ const (
|
|||||||
Direct AdapterType = iota
|
Direct AdapterType = iota
|
||||||
Reject
|
Reject
|
||||||
Compatible
|
Compatible
|
||||||
|
Mitm
|
||||||
Pass
|
Pass
|
||||||
|
|
||||||
Relay
|
Relay
|
||||||
@ -182,6 +183,8 @@ func (at AdapterType) String() string {
|
|||||||
return "Compatible"
|
return "Compatible"
|
||||||
case Pass:
|
case Pass:
|
||||||
return "Pass"
|
return "Pass"
|
||||||
|
case Mitm:
|
||||||
|
return "Mitm"
|
||||||
case Shadowsocks:
|
case Shadowsocks:
|
||||||
return "Shadowsocks"
|
return "Shadowsocks"
|
||||||
case ShadowsocksR:
|
case ShadowsocksR:
|
||||||
|
@ -31,6 +31,7 @@ const (
|
|||||||
TUN
|
TUN
|
||||||
TUIC
|
TUIC
|
||||||
INNER
|
INNER
|
||||||
|
MITM
|
||||||
)
|
)
|
||||||
|
|
||||||
type NetWork int
|
type NetWork int
|
||||||
@ -80,6 +81,8 @@ func (t Type) String() string {
|
|||||||
return "Tuic"
|
return "Tuic"
|
||||||
case INNER:
|
case INNER:
|
||||||
return "Inner"
|
return "Inner"
|
||||||
|
case MITM:
|
||||||
|
return "Mitm"
|
||||||
default:
|
default:
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
}
|
}
|
||||||
@ -144,6 +147,8 @@ type Metadata struct {
|
|||||||
RemoteDst string `json:"remoteDestination"`
|
RemoteDst string `json:"remoteDestination"`
|
||||||
// Only domain rule
|
// Only domain rule
|
||||||
SniffHost string `json:"sniffHost"`
|
SniffHost string `json:"sniffHost"`
|
||||||
|
// Only Mitm rule
|
||||||
|
UserAgent string `json:"userAgent"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Metadata) RemoteAddress() string {
|
func (m *Metadata) RemoteAddress() string {
|
||||||
|
@ -148,8 +148,12 @@ 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *path) GetExecutableFullPath() string {
|
func (p *path) GetExecutableFullPath() string {
|
||||||
|
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
|
||||||
|
}
|
@ -23,6 +23,7 @@ const (
|
|||||||
Network
|
Network
|
||||||
Uid
|
Uid
|
||||||
SubRules
|
SubRules
|
||||||
|
UserAgent
|
||||||
MATCH
|
MATCH
|
||||||
AND
|
AND
|
||||||
OR
|
OR
|
||||||
@ -67,6 +68,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"
|
||||||
case RuleSet:
|
case RuleSet:
|
||||||
|
4
go.mod
4
go.mod
@ -12,6 +12,7 @@ require (
|
|||||||
github.com/go-chi/chi/v5 v5.0.10
|
github.com/go-chi/chi/v5 v5.0.10
|
||||||
github.com/go-chi/cors v1.2.1
|
github.com/go-chi/cors v1.2.1
|
||||||
github.com/go-chi/render v1.0.3
|
github.com/go-chi/render v1.0.3
|
||||||
|
github.com/gofrs/uuid v4.4.0+incompatible
|
||||||
github.com/gofrs/uuid/v5 v5.0.0
|
github.com/gofrs/uuid/v5 v5.0.0
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20230731140434-0f9eb93a696c
|
github.com/insomniacslk/dhcp v0.0.0-20230731140434-0f9eb93a696c
|
||||||
@ -44,12 +45,14 @@ require (
|
|||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
github.com/zhangyunhao116/fastrand v0.3.0
|
github.com/zhangyunhao116/fastrand v0.3.0
|
||||||
go.etcd.io/bbolt v1.3.7
|
go.etcd.io/bbolt v1.3.7
|
||||||
|
go.uber.org/atomic v1.9.0
|
||||||
go.uber.org/automaxprocs v1.5.3
|
go.uber.org/automaxprocs v1.5.3
|
||||||
golang.org/x/crypto v0.12.0
|
golang.org/x/crypto v0.12.0
|
||||||
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb
|
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb
|
||||||
golang.org/x/net v0.14.0
|
golang.org/x/net v0.14.0
|
||||||
golang.org/x/sync v0.3.0
|
golang.org/x/sync v0.3.0
|
||||||
golang.org/x/sys v0.11.0
|
golang.org/x/sys v0.11.0
|
||||||
|
golang.org/x/text v0.12.0
|
||||||
google.golang.org/protobuf v1.31.0
|
google.golang.org/protobuf v1.31.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
lukechampine.com/blake3 v1.2.1
|
lukechampine.com/blake3 v1.2.1
|
||||||
@ -100,7 +103,6 @@ require (
|
|||||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
|
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
|
||||||
golang.org/x/mod v0.11.0 // indirect
|
golang.org/x/mod v0.11.0 // indirect
|
||||||
golang.org/x/text v0.12.0 // indirect
|
|
||||||
golang.org/x/time v0.3.0 // indirect
|
golang.org/x/time v0.3.0 // indirect
|
||||||
golang.org/x/tools v0.9.1 // indirect
|
golang.org/x/tools v0.9.1 // indirect
|
||||||
)
|
)
|
||||||
|
4
go.sum
4
go.sum
@ -50,6 +50,8 @@ github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
|||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||||
|
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
||||||
|
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
|
github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
|
||||||
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||||
@ -206,6 +208,8 @@ gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiV
|
|||||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ=
|
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ=
|
||||||
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
|
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
|
||||||
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||||
|
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||||
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
|
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
|
||||||
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
|
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
@ -91,10 +91,11 @@ func ApplyConfig(cfg *config.Config, force bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateUsers(cfg.Users)
|
updateUsers(cfg.Users)
|
||||||
updateProxies(cfg.Proxies, cfg.Providers)
|
updateProxies(cfg.Mitm, cfg.Proxies, cfg.Providers)
|
||||||
updateRules(cfg.Rules, cfg.SubRules, cfg.RuleProviders)
|
updateRules(cfg.Rules, cfg.SubRules, cfg.RuleProviders)
|
||||||
updateSniffer(cfg.Sniffer)
|
updateSniffer(cfg.Sniffer)
|
||||||
updateHosts(cfg.Hosts)
|
updateHosts(cfg.Hosts)
|
||||||
|
updateMitm(cfg.Mitm)
|
||||||
updateGeneral(cfg.General)
|
updateGeneral(cfg.General)
|
||||||
updateNTP(cfg.NTP)
|
updateNTP(cfg.NTP)
|
||||||
updateDNS(cfg.DNS, cfg.RuleProviders, cfg.General.IPv6)
|
updateDNS(cfg.DNS, cfg.RuleProviders, cfg.General.IPv6)
|
||||||
@ -134,6 +135,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,
|
||||||
Tun: listener.GetTunConf(),
|
Tun: listener.GetTunConf(),
|
||||||
TuicServer: listener.GetTuicConf(),
|
TuicServer: listener.GetTuicConf(),
|
||||||
ShadowSocksConfig: ports.ShadowSocksConfig,
|
ShadowSocksConfig: ports.ShadowSocksConfig,
|
||||||
@ -262,7 +264,7 @@ func updateHosts(tree *trie.DomainTrie[resolver.HostValue]) {
|
|||||||
resolver.DefaultHosts = resolver.NewHosts(tree)
|
resolver.DefaultHosts = resolver.NewHosts(tree)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateProxies(proxies map[string]C.Proxy, providers map[string]provider.ProxyProvider) {
|
func updateProxies(mitm *config.Mitm, proxies map[string]C.Proxy, providers map[string]provider.ProxyProvider) {
|
||||||
tunnel.UpdateProxies(proxies, providers)
|
tunnel.UpdateProxies(proxies, providers)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -490,6 +492,11 @@ func updateIPTables(cfg *config.Config) {
|
|||||||
log.Infoln("[IPTABLES] Setting iptables completed")
|
log.Infoln("[IPTABLES] Setting iptables completed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateMitm(mitm *config.Mitm) {
|
||||||
|
listener.ReCreateMitm(mitm.Port, tunnel.TCPIn())
|
||||||
|
tunnel.UpdateRewrites(mitm.Rules)
|
||||||
|
}
|
||||||
|
|
||||||
func Shutdown() {
|
func Shutdown() {
|
||||||
listener.Cleanup()
|
listener.Cleanup()
|
||||||
tproxy.CleanupTProxyIPTables()
|
tproxy.CleanupTProxyIPTables()
|
||||||
|
@ -40,6 +40,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 *tunSchema `json:"tun"`
|
Tun *tunSchema `json:"tun"`
|
||||||
TuicServer *tuicServerSchema `json:"tuic-server"`
|
TuicServer *tuicServerSchema `json:"tuic-server"`
|
||||||
ShadowSocksConfig *string `json:"ss-config"`
|
ShadowSocksConfig *string `json:"ss-config"`
|
||||||
@ -262,6 +263,7 @@ func patchConfigs(w http.ResponseWriter, r *http.Request) {
|
|||||||
P.ReCreateShadowSocks(pointerOrDefaultString(general.ShadowSocksConfig, ports.ShadowSocksConfig), tcpIn, udpIn)
|
P.ReCreateShadowSocks(pointerOrDefaultString(general.ShadowSocksConfig, ports.ShadowSocksConfig), tcpIn, udpIn)
|
||||||
P.ReCreateVmess(pointerOrDefaultString(general.VmessConfig, ports.VmessConfig), tcpIn, udpIn)
|
P.ReCreateVmess(pointerOrDefaultString(general.VmessConfig, ports.VmessConfig), tcpIn, udpIn)
|
||||||
P.ReCreateTuic(pointerOrDefaultTuicServer(general.TuicServer, P.LastTuicConf), tcpIn, udpIn)
|
P.ReCreateTuic(pointerOrDefaultTuicServer(general.TuicServer, P.LastTuicConf), 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)
|
||||||
|
@ -36,7 +36,7 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.LruCache[strin
|
|||||||
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.LruCache[strin
|
|||||||
return // hijack connection
|
return // hijack connection
|
||||||
}
|
}
|
||||||
|
|
||||||
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.LruCache[strin
|
|||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func authenticate(request *http.Request, cache *cache.LruCache[string, bool]) *http.Response {
|
func Authenticate(request *http.Request, cache *cache.LruCache[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.LruCache[string, bool]) *h
|
|||||||
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),
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapter/inbound"
|
"github.com/Dreamacro/clash/adapter/inbound"
|
||||||
N "github.com/Dreamacro/clash/common/net"
|
N "github.com/Dreamacro/clash/common/net"
|
||||||
@ -29,7 +30,7 @@ func handleUpgrade(conn net.Conn, request *http.Request, in chan<- C.ConnContext
|
|||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
removeProxyHeaders(request.Header)
|
removeProxyHeaders(request.Header)
|
||||||
removeExtraHTTPHostPort(request)
|
RemoveExtraHTTPHostPort(request)
|
||||||
|
|
||||||
address := request.Host
|
address := request.Host
|
||||||
if _, _, err := net.SplitHostPort(address); err != nil {
|
if _, _, err := net.SplitHostPort(address); err != nil {
|
||||||
@ -87,3 +88,65 @@ func handleUpgrade(conn net.Conn, request *http.Request, in chan<- C.ConnContext
|
|||||||
N.Relay(bufferedLeft, conn)
|
N.Relay(bufferedLeft, conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func HandleUpgradeY(localConn net.Conn, serverConn *N.BufferedConn, request *http.Request, in chan<- C.ConnContext) (resp *http.Response) {
|
||||||
|
removeProxyHeaders(request.Header)
|
||||||
|
RemoveExtraHTTPHostPort(request)
|
||||||
|
|
||||||
|
if serverConn == nil {
|
||||||
|
address := request.Host
|
||||||
|
if _, _, err := net.SplitHostPort(address); err != nil {
|
||||||
|
port := "80"
|
||||||
|
if request.TLS != nil {
|
||||||
|
port = "443"
|
||||||
|
}
|
||||||
|
address = net.JoinHostPort(address, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
dstAddr := socks5.ParseAddr(address)
|
||||||
|
if dstAddr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
left, right := net.Pipe()
|
||||||
|
|
||||||
|
in <- inbound.NewHTTP(dstAddr, localConn.RemoteAddr(), right)
|
||||||
|
|
||||||
|
serverConn = N.NewBufferedConn(left)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = serverConn.Close()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := request.Write(serverConn)
|
||||||
|
if err != nil {
|
||||||
|
_ = localConn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = http.ReadResponse(serverConn.Reader(), request)
|
||||||
|
if err != nil {
|
||||||
|
_ = localConn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusSwitchingProtocols {
|
||||||
|
removeProxyHeaders(resp.Header)
|
||||||
|
|
||||||
|
err = localConn.SetReadDeadline(time.Time{}) // set to not time out
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = resp.Write(localConn)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
N.Relay(serverConn, localConn) // blocking here
|
||||||
|
_ = localConn.Close()
|
||||||
|
resp = nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
@ -15,8 +15,8 @@ func removeProxyHeaders(header http.Header) {
|
|||||||
header.Del("Proxy-Authorization")
|
header.Del("Proxy-Authorization")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@ -38,9 +38,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,19 +1,26 @@
|
|||||||
package listener
|
package listener
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/cert"
|
||||||
"github.com/Dreamacro/clash/component/ebpf"
|
"github.com/Dreamacro/clash/component/ebpf"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/listener/autoredir"
|
"github.com/Dreamacro/clash/listener/autoredir"
|
||||||
LC "github.com/Dreamacro/clash/listener/config"
|
LC "github.com/Dreamacro/clash/listener/config"
|
||||||
"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"
|
||||||
embedSS "github.com/Dreamacro/clash/listener/shadowsocks"
|
embedSS "github.com/Dreamacro/clash/listener/shadowsocks"
|
||||||
@ -23,9 +30,10 @@ import (
|
|||||||
"github.com/Dreamacro/clash/listener/socks"
|
"github.com/Dreamacro/clash/listener/socks"
|
||||||
"github.com/Dreamacro/clash/listener/tproxy"
|
"github.com/Dreamacro/clash/listener/tproxy"
|
||||||
"github.com/Dreamacro/clash/listener/tuic"
|
"github.com/Dreamacro/clash/listener/tuic"
|
||||||
"github.com/Dreamacro/clash/listener/tunnel"
|
LT "github.com/Dreamacro/clash/listener/tunnel"
|
||||||
"github.com/Dreamacro/clash/log"
|
"github.com/Dreamacro/clash/log"
|
||||||
|
|
||||||
|
rewrites "github.com/Dreamacro/clash/rewrite"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -42,8 +50,8 @@ var (
|
|||||||
tproxyUDPListener *tproxy.UDPListener
|
tproxyUDPListener *tproxy.UDPListener
|
||||||
mixedListener *mixed.Listener
|
mixedListener *mixed.Listener
|
||||||
mixedUDPLister *socks.UDPListener
|
mixedUDPLister *socks.UDPListener
|
||||||
tunnelTCPListeners = map[string]*tunnel.Listener{}
|
tunnelTCPListeners = map[string]*LT.Listener{}
|
||||||
tunnelUDPListeners = map[string]*tunnel.PacketConn{}
|
tunnelUDPListeners = map[string]*LT.PacketConn{}
|
||||||
inboundListeners = map[string]C.InboundListener{}
|
inboundListeners = map[string]C.InboundListener{}
|
||||||
tunLister *sing_tun.Listener
|
tunLister *sing_tun.Listener
|
||||||
shadowSocksListener C.MultiAddrListener
|
shadowSocksListener C.MultiAddrListener
|
||||||
@ -52,6 +60,7 @@ var (
|
|||||||
autoRedirListener *autoredir.Listener
|
autoRedirListener *autoredir.Listener
|
||||||
autoRedirProgram *ebpf.TcEBpfProgram
|
autoRedirProgram *ebpf.TcEBpfProgram
|
||||||
tcProgram *ebpf.TcEBpfProgram
|
tcProgram *ebpf.TcEBpfProgram
|
||||||
|
mitmListener *mitm.Listener
|
||||||
|
|
||||||
// lock for recreate function
|
// lock for recreate function
|
||||||
socksMux sync.Mutex
|
socksMux sync.Mutex
|
||||||
@ -67,6 +76,7 @@ var (
|
|||||||
tuicMux sync.Mutex
|
tuicMux sync.Mutex
|
||||||
autoRedirMux sync.Mutex
|
autoRedirMux sync.Mutex
|
||||||
tcMux sync.Mutex
|
tcMux sync.Mutex
|
||||||
|
mitmMux sync.Mutex
|
||||||
|
|
||||||
LastTunConf LC.Tun
|
LastTunConf LC.Tun
|
||||||
LastTuicConf LC.TuicServer
|
LastTuicConf LC.TuicServer
|
||||||
@ -80,6 +90,7 @@ type Ports struct {
|
|||||||
MixedPort int `json:"mixed-port"`
|
MixedPort int `json:"mixed-port"`
|
||||||
ShadowSocksConfig string `json:"ss-config"`
|
ShadowSocksConfig string `json:"ss-config"`
|
||||||
VmessConfig string `json:"vmess-config"`
|
VmessConfig string `json:"vmess-config"`
|
||||||
|
MitmPort int `json:"mitm-port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetTunConf() LC.Tun {
|
func GetTunConf() LC.Tun {
|
||||||
@ -699,7 +710,7 @@ func PatchTunnel(tunnels []LC.Tunnel, tcpIn chan<- C.ConnContext, udpIn chan<- C
|
|||||||
for _, elm := range needCreate {
|
for _, elm := range needCreate {
|
||||||
key := fmt.Sprintf("%s/%s/%s", elm.addr, elm.target, elm.proxy)
|
key := fmt.Sprintf("%s/%s/%s", elm.addr, elm.target, elm.proxy)
|
||||||
if elm.network == "tcp" {
|
if elm.network == "tcp" {
|
||||||
l, err := tunnel.New(elm.addr, elm.target, elm.proxy, tcpIn)
|
l, err := LT.New(elm.addr, elm.target, elm.proxy, tcpIn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("Start tunnel %s error: %s", elm.target, err.Error())
|
log.Errorln("Start tunnel %s error: %s", elm.target, err.Error())
|
||||||
continue
|
continue
|
||||||
@ -707,7 +718,7 @@ func PatchTunnel(tunnels []LC.Tunnel, tcpIn chan<- C.ConnContext, udpIn chan<- C
|
|||||||
tunnelTCPListeners[key] = l
|
tunnelTCPListeners[key] = l
|
||||||
log.Infoln("Tunnel(tcp/%s) proxy %s listening at: %s", elm.target, elm.proxy, tunnelTCPListeners[key].Address())
|
log.Infoln("Tunnel(tcp/%s) proxy %s listening at: %s", elm.target, elm.proxy, tunnelTCPListeners[key].Address())
|
||||||
} else {
|
} else {
|
||||||
l, err := tunnel.NewUDP(elm.addr, elm.target, elm.proxy, udpIn)
|
l, err := LT.NewUDP(elm.addr, elm.target, elm.proxy, udpIn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("Start tunnel %s error: %s", elm.target, err.Error())
|
log.Errorln("Start tunnel %s error: %s", elm.target, err.Error())
|
||||||
continue
|
continue
|
||||||
@ -747,6 +758,79 @@ func PatchInboundListeners(newListenerMap map[string]C.InboundListener, tcpIn ch
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
_ = 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,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
certOption.SetValidity(time.Hour * 24 * 365 * 2) // 2 years
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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{}
|
||||||
@ -789,6 +873,12 @@ func GetPorts() *Ports {
|
|||||||
ports.VmessConfig = vmessListener.Config()
|
ports.VmessConfig = vmessListener.Config()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if mitmListener != nil {
|
||||||
|
_, portStr, _ := net.SplitHostPort(mitmListener.Address())
|
||||||
|
port, _ := strconv.Atoi(portStr)
|
||||||
|
ports.MitmPort = port
|
||||||
|
}
|
||||||
|
|
||||||
return ports
|
return ports
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -902,6 +992,19 @@ func closeTunListener() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
closeTunListener()
|
closeTunListener()
|
||||||
}
|
}
|
||||||
|
55
listener/mitm/client.go
Normal file
55
listener/mitm/client.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package mitm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/adapter/inbound"
|
||||||
|
N "github.com/Dreamacro/clash/common/net"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/transport/socks5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getServerConn(serverConn *N.BufferedConn, request *http.Request, srcAddr net.Addr, in chan<- C.ConnContext) (*N.BufferedConn, error) {
|
||||||
|
if serverConn != nil {
|
||||||
|
return serverConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
address := request.URL.Host
|
||||||
|
if _, _, err := net.SplitHostPort(address); err != nil {
|
||||||
|
port := "80"
|
||||||
|
if request.TLS != nil {
|
||||||
|
port = "443"
|
||||||
|
}
|
||||||
|
address = net.JoinHostPort(address, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
dstAddr := socks5.ParseAddr(address)
|
||||||
|
if dstAddr == nil {
|
||||||
|
return nil, socks5.ErrAddressNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
left, right := net.Pipe()
|
||||||
|
|
||||||
|
in <- inbound.NewMitm(dstAddr, srcAddr, request.Header.Get("User-Agent"), right)
|
||||||
|
|
||||||
|
if request.TLS != nil {
|
||||||
|
tlsConn := tls.Client(left, &tls.Config{
|
||||||
|
ServerName: request.TLS.ServerName,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
|
||||||
|
defer cancel()
|
||||||
|
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
serverConn = N.NewBufferedConn(tlsConn)
|
||||||
|
} else {
|
||||||
|
serverConn = N.NewBufferedConn(left)
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverConn, nil
|
||||||
|
}
|
9
listener/mitm/hack.go
Normal file
9
listener/mitm/hack.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package mitm
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "net/http"
|
||||||
|
_ "unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:linkname validMethod net/http.validMethod
|
||||||
|
func validMethod(method string) bool
|
349
listener/mitm/proxy.go
Normal file
349
listener/mitm/proxy.go
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
package mitm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/cache"
|
||||||
|
N "github.com/Dreamacro/clash/common/net"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
H "github.com/Dreamacro/clash/listener/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleConn(c net.Conn, opt *Option, in chan<- C.ConnContext, cache *cache.LruCache[string, bool]) {
|
||||||
|
var (
|
||||||
|
clientIP = netip.MustParseAddrPort(c.RemoteAddr().String()).Addr()
|
||||||
|
sourceAddr net.Addr
|
||||||
|
serverConn *N.BufferedConn
|
||||||
|
connState *tls.ConnectionState
|
||||||
|
)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if serverConn != nil {
|
||||||
|
_ = serverConn.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
conn := N.NewBufferedConn(c)
|
||||||
|
|
||||||
|
trusted := cache == nil // disable authenticate if cache is nil
|
||||||
|
if !trusted {
|
||||||
|
trusted = clientIP.IsLoopback() || clientIP.IsUnspecified()
|
||||||
|
}
|
||||||
|
|
||||||
|
readLoop:
|
||||||
|
for {
|
||||||
|
// use SetReadDeadline instead of Proxy-Connection keep-alive
|
||||||
|
if err := conn.SetReadDeadline(time.Now().Add(65 * time.Second)); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := H.ReadRequest(conn.Reader())
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var response *http.Response
|
||||||
|
|
||||||
|
session := newSession(conn, request, response)
|
||||||
|
|
||||||
|
sourceAddr = parseSourceAddress(session.request, conn.RemoteAddr(), sourceAddr)
|
||||||
|
session.request.RemoteAddr = sourceAddr.String()
|
||||||
|
|
||||||
|
if !trusted {
|
||||||
|
session.response = H.Authenticate(session.request, cache)
|
||||||
|
|
||||||
|
trusted = session.response == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if trusted {
|
||||||
|
if session.request.Method == http.MethodConnect {
|
||||||
|
if session.request.ProtoMajor > 1 {
|
||||||
|
session.request.ProtoMajor = 1
|
||||||
|
session.request.ProtoMinor = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 // close connection
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(session.request.URL.Host, ":80") {
|
||||||
|
goto readLoop
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err1 := conn.Peek(1)
|
||||||
|
if err1 != nil {
|
||||||
|
handleError(opt, session, err1)
|
||||||
|
break // close connection
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLS handshake.
|
||||||
|
if b[0] == 0x16 {
|
||||||
|
tlsConn := tls.Server(conn, opt.CertConfig.NewTLSConfigForHost(session.request.URL.Hostname()))
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
|
||||||
|
// handshake with the local client
|
||||||
|
if err = tlsConn.HandshakeContext(ctx); err != nil {
|
||||||
|
cancel()
|
||||||
|
session.response = session.NewErrorResponse(fmt.Errorf("handshake failed: %w", err))
|
||||||
|
_ = writeResponse(session, false)
|
||||||
|
break // close connection
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
cs := tlsConn.ConnectionState()
|
||||||
|
connState = &cs
|
||||||
|
|
||||||
|
conn = N.NewBufferedConn(tlsConn)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(session.request.URL.Host, ":443") {
|
||||||
|
goto readLoop
|
||||||
|
}
|
||||||
|
|
||||||
|
if conn.SetReadDeadline(time.Now().Add(time.Second)) != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err2 := conn.Peek(7)
|
||||||
|
if err2 != nil {
|
||||||
|
if err2 != bufio.ErrBufferFull && !os.IsTimeout(err2) {
|
||||||
|
handleError(opt, session, err2)
|
||||||
|
break // close connection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// others protocol over tcp
|
||||||
|
if !isHTTPTraffic(buf) {
|
||||||
|
if connState != nil {
|
||||||
|
session.request.TLS = connState
|
||||||
|
}
|
||||||
|
|
||||||
|
serverConn, err = getServerConn(serverConn, session.request, sourceAddr, in)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if conn.SetReadDeadline(time.Time{}) != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
N.Relay(serverConn, conn)
|
||||||
|
return // hijack connection
|
||||||
|
}
|
||||||
|
|
||||||
|
goto readLoop
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareRequest(connState, session.request)
|
||||||
|
|
||||||
|
// hijack api
|
||||||
|
if session.request.URL.Hostname() == opt.ApiHost {
|
||||||
|
if err = handleApiRequest(session, opt); err != nil {
|
||||||
|
handleError(opt, session, err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// forward websocket
|
||||||
|
if isWebsocketRequest(request) {
|
||||||
|
serverConn, err = getServerConn(serverConn, session.request, sourceAddr, in)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
session.request.RequestURI = ""
|
||||||
|
if session.response = H.HandleUpgradeY(conn, serverConn, request, in); session.response == nil {
|
||||||
|
return // hijack connection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.response == nil {
|
||||||
|
H.RemoveHopByHopHeaders(session.request.Header)
|
||||||
|
H.RemoveExtraHTTPHostPort(session.request)
|
||||||
|
|
||||||
|
// hijack custom request and write back custom response if necessary
|
||||||
|
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
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
session.request.RequestURI = ""
|
||||||
|
|
||||||
|
if session.request.URL.Host == "" {
|
||||||
|
session.response = session.NewErrorResponse(ErrInvalidURL)
|
||||||
|
} else {
|
||||||
|
serverConn, err = getServerConn(serverConn, session.request, sourceAddr, in)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// send the request to remote server
|
||||||
|
err = session.request.Write(serverConn)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
session.response, err = http.ReadResponse(serverConn.Reader(), request)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = writeResponseWithHandler(session, opt); err != nil {
|
||||||
|
handleError(opt, session, err)
|
||||||
|
break // close connection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeResponseWithHandler(session *Session, opt *Option) error {
|
||||||
|
res := opt.Handler.HandleResponse(session)
|
||||||
|
if res != nil {
|
||||||
|
session.response = res
|
||||||
|
}
|
||||||
|
|
||||||
|
return writeResponse(session, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeResponse(session *Session, keepAlive bool) error {
|
||||||
|
H.RemoveHopByHopHeaders(session.response.Header)
|
||||||
|
|
||||||
|
if keepAlive {
|
||||||
|
session.response.Header.Set("Connection", "keep-alive")
|
||||||
|
session.response.Header.Set("Keep-Alive", "timeout=60")
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.writeResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
session.response.Close = true
|
||||||
|
session.response.Header.Set("Content-Type", "application/x-x509-ca-cert")
|
||||||
|
session.response.ContentLength = int64(len(b))
|
||||||
|
|
||||||
|
return session.writeResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
b := `<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
|
||||||
|
<html><head>
|
||||||
|
<title>Clash MITM 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.HandleApiRequest(session) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b = fmt.Sprintf(b, session.request.URL.Path)
|
||||||
|
|
||||||
|
session.response = session.NewResponse(http.StatusNotFound, bytes.NewReader([]byte(b)))
|
||||||
|
session.response.Close = true
|
||||||
|
session.response.Header.Set("Content-Type", "text/html;charset=utf-8")
|
||||||
|
session.response.ContentLength = int64(len(b))
|
||||||
|
|
||||||
|
return session.writeResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleError(opt *Option, session *Session, err error) {
|
||||||
|
if session.response != nil {
|
||||||
|
defer func() {
|
||||||
|
_, _ = io.Copy(io.Discard, session.response.Body)
|
||||||
|
_ = session.response.Body.Close()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
opt.Handler.HandleError(session, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareRequest(connState *tls.ConnectionState, request *http.Request) {
|
||||||
|
host := request.Header.Get("Host")
|
||||||
|
if host != "" {
|
||||||
|
request.Host = host
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.URL.Host == "" {
|
||||||
|
request.URL.Host = request.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.URL.Scheme == "" {
|
||||||
|
request.URL.Scheme = "http"
|
||||||
|
}
|
||||||
|
|
||||||
|
if connState != nil {
|
||||||
|
request.TLS = connState
|
||||||
|
request.URL.Scheme = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.Header.Get("Accept-Encoding") != "" {
|
||||||
|
request.Header.Set("Accept-Encoding", "gzip")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSourceAddress(req *http.Request, connSource, source net.Addr) net.Addr {
|
||||||
|
if source != nil {
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceAddress := req.Header.Get("Origin-Request-Source-Address")
|
||||||
|
if sourceAddress == "" {
|
||||||
|
return connSource
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Del("Origin-Request-Source-Address")
|
||||||
|
|
||||||
|
addrPort, err := netip.ParseAddrPort(sourceAddress)
|
||||||
|
if err != nil {
|
||||||
|
return connSource
|
||||||
|
}
|
||||||
|
|
||||||
|
return &net.TCPAddr{
|
||||||
|
IP: addrPort.Addr().AsSlice(),
|
||||||
|
Port: int(addrPort.Port()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWebsocketRequest(req *http.Request) bool {
|
||||||
|
return strings.EqualFold(req.Header.Get("Connection"), "Upgrade") && strings.EqualFold(req.Header.Get("Upgrade"), "websocket")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isHTTPTraffic(buf []byte) bool {
|
||||||
|
method, _, _ := strings.Cut(string(buf), " ")
|
||||||
|
return validMethod(method)
|
||||||
|
}
|
88
listener/mitm/server.go
Normal file
88
listener/mitm/server.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package mitm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"github.com/Dreamacro/clash/common/cache"
|
||||||
|
"github.com/Dreamacro/clash/common/cert"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
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, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
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.LruCache[string, bool]
|
||||||
|
if authenticate {
|
||||||
|
c = cache.New[string, bool](cache.WithAge[string, bool](90))
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
59
listener/mitm/session.go
Normal file
59
listener/mitm/session.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package mitm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return NewResponse(code, body, s.request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) NewErrorResponse(err error) *http.Response {
|
||||||
|
return NewErrorResponse(s.request, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) writeResponse() error {
|
||||||
|
if s.response == nil {
|
||||||
|
return ErrInvalidResponse
|
||||||
|
}
|
||||||
|
defer func(resp *http.Response) {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}(s.response)
|
||||||
|
return s.response.Write(s.conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSession(conn net.Conn, request *http.Request, response *http.Response) *Session {
|
||||||
|
return &Session{
|
||||||
|
conn: conn,
|
||||||
|
request: request,
|
||||||
|
response: response,
|
||||||
|
props: map[string]any{},
|
||||||
|
}
|
||||||
|
}
|
95
listener/mitm/utils.go
Normal file
95
listener/mitm/utils.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package mitm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/text/encoding/charmap"
|
||||||
|
"golang.org/x/text/transform"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidResponse = errors.New("invalid response")
|
||||||
|
ErrInvalidURL = errors.New("invalid URL")
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
}
|
52
rules/common/user_gent.go
Normal file
52
rules/common/user_gent.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
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, string) {
|
||||||
|
if metadata.Type != C.MITM || metadata.UserAgent == "" {
|
||||||
|
return false, d.adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Contains(metadata.UserAgent, d.ua), d.adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
11
test/go.mod
11
test/go.mod
@ -97,10 +97,12 @@ require (
|
|||||||
github.com/tklauser/numcpus v0.6.0 // indirect
|
github.com/tklauser/numcpus v0.6.0 // indirect
|
||||||
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
|
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
|
||||||
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
|
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
|
||||||
|
github.com/xtls/go v0.0.0-20210920065950-d4af136d3672 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||||
github.com/zhangyunhao116/fastrand v0.3.0 // indirect
|
github.com/zhangyunhao116/fastrand v0.3.0 // indirect
|
||||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
|
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
|
||||||
go.etcd.io/bbolt v1.3.7 // indirect
|
go.etcd.io/bbolt v1.3.7 // indirect
|
||||||
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
golang.org/x/crypto v0.12.0 // indirect
|
golang.org/x/crypto v0.12.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb // indirect
|
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb // indirect
|
||||||
golang.org/x/mod v0.11.0 // indirect
|
golang.org/x/mod v0.11.0 // indirect
|
||||||
@ -109,7 +111,16 @@ require (
|
|||||||
golang.org/x/text v0.12.0 // indirect
|
golang.org/x/text v0.12.0 // indirect
|
||||||
golang.org/x/time v0.3.0 // indirect
|
golang.org/x/time v0.3.0 // indirect
|
||||||
golang.org/x/tools v0.9.1 // indirect
|
golang.org/x/tools v0.9.1 // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||||
|
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
|
||||||
|
golang.zx2c4.com/wireguard v0.0.0-20220318042302-193cf8d6a5d6 // indirect
|
||||||
|
golang.zx2c4.com/wireguard/windows v0.5.4-0.20220317000008-6432784c2469 // indirect
|
||||||
|
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
|
||||||
|
google.golang.org/grpc v1.53.0-dev.0.20230123225046-4075ef07c5d5 // indirect
|
||||||
google.golang.org/protobuf v1.31.0 // indirect
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
gotest.tools/v3 v3.4.0 // indirect
|
||||||
|
gvisor.dev/gvisor v0.0.0-20220326024801-5d1f3d24cb84 // indirect
|
||||||
lukechampine.com/blake3 v1.2.1 // indirect
|
lukechampine.com/blake3 v1.2.1 // indirect
|
||||||
)
|
)
|
||||||
|
1495
test/go.sum
1495
test/go.sum
File diff suppressed because it is too large
Load Diff
@ -30,6 +30,7 @@ var (
|
|||||||
udpQueue = make(chan C.PacketAdapter, 200)
|
udpQueue = make(chan C.PacketAdapter, 200)
|
||||||
natTable = nat.New()
|
natTable = nat.New()
|
||||||
rules []C.Rule
|
rules []C.Rule
|
||||||
|
rewrites C.RewriteRule
|
||||||
listeners = make(map[string]C.InboundListener)
|
listeners = make(map[string]C.InboundListener)
|
||||||
subRules map[string][]C.Rule
|
subRules map[string][]C.Rule
|
||||||
proxies = make(map[string]C.Proxy)
|
proxies = make(map[string]C.Proxy)
|
||||||
@ -179,6 +180,18 @@ func isHandle(t C.Type) bool {
|
|||||||
return status == Running || (status == Inner && t == C.INNER)
|
return status == Running || (status == Inner && t == C.INNER)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rewrites return all rewrites
|
||||||
|
func Rewrites() C.RewriteRule {
|
||||||
|
return rewrites
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRewrites handle update rewrites
|
||||||
|
func UpdateRewrites(rules C.RewriteRule) {
|
||||||
|
configMux.Lock()
|
||||||
|
rewrites = rules
|
||||||
|
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
|
||||||
@ -433,8 +446,9 @@ func handleTCPConn(connCtx C.ConnContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isMitmProxy := metadata.Type == C.MITM
|
||||||
dialMetadata := metadata
|
dialMetadata := metadata
|
||||||
if len(metadata.Host) > 0 {
|
if len(metadata.Host) > 0 && !isMitmProxy {
|
||||||
if node, ok := resolver.DefaultHosts.Search(metadata.Host, false); ok {
|
if node, ok := resolver.DefaultHosts.Search(metadata.Host, false); ok {
|
||||||
if dstIp, _ := node.RandIP(); !FakeIPRange().Contains(dstIp) {
|
if dstIp, _ := node.RandIP(); !FakeIPRange().Contains(dstIp) {
|
||||||
dialMetadata.DstIP = dstIp
|
dialMetadata.DstIP = dstIp
|
||||||
|
Loading…
Reference in New Issue
Block a user