mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2024-11-16 03:32:33 +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))
|
||||
}
|
||||
|
||||
if metadata.Type == C.MITM {
|
||||
tempHeaders["Origin-Request-Source-Address"] = metadata.SourceAddress()
|
||||
}
|
||||
|
||||
for key, value := range tempHeaders {
|
||||
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) {
|
||||
asset := C.Path.GetAssetLocation(filename)
|
||||
asset := C.Path.Resolve(filename)
|
||||
idx := strings.ToLower(asset + ":" + code)
|
||||
if g.Has(idx) {
|
||||
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) {
|
||||
asset := C.Path.GetAssetLocation(filename)
|
||||
asset := C.Path.Resolve(filename)
|
||||
idx := strings.ToLower(asset + ":" + code)
|
||||
if g.Has(idx) {
|
||||
return g.Get(idx), nil
|
||||
|
|
|
@ -26,7 +26,7 @@ func ReadFile(path string) ([]byte, error) {
|
|||
}
|
||||
|
||||
func ReadAsset(file string) ([]byte, error) {
|
||||
return ReadFile(C.Path.GetAssetLocation(file))
|
||||
return ReadFile(C.Path.Resolve(file))
|
||||
}
|
||||
|
||||
func loadIP(geoipBytes []byte, country string) ([]*router.CIDR, error) {
|
||||
|
|
|
@ -35,6 +35,7 @@ import (
|
|||
L "github.com/Dreamacro/clash/listener"
|
||||
LC "github.com/Dreamacro/clash/listener/config"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
rewrites "github.com/Dreamacro/clash/rewrite"
|
||||
R "github.com/Dreamacro/clash/rules"
|
||||
RP "github.com/Dreamacro/clash/rules/provider"
|
||||
T "github.com/Dreamacro/clash/tunnel"
|
||||
|
@ -79,6 +80,7 @@ type Inbound struct {
|
|||
BindAddress string `json:"bind-address"`
|
||||
InboundTfo bool `json:"inbound-tfo"`
|
||||
InboundMPTCP bool `json:"inbound-mptcp"`
|
||||
MitmPort int `json:"mitm-port"`
|
||||
}
|
||||
|
||||
// Controller config
|
||||
|
@ -152,6 +154,12 @@ type Sniffer struct {
|
|||
ParsePureIp bool
|
||||
}
|
||||
|
||||
// Mitm config
|
||||
type Mitm struct {
|
||||
Port int `yaml:"port" json:"port"`
|
||||
Rules C.RewriteRule `yaml:"rules" json:"rules"`
|
||||
}
|
||||
|
||||
// Experimental config
|
||||
type Experimental struct {
|
||||
Fingerprints []string `yaml:"fingerprints"`
|
||||
|
@ -161,6 +169,7 @@ type Experimental struct {
|
|||
type Config struct {
|
||||
General *General
|
||||
IPTables *IPTables
|
||||
Mitm *Mitm
|
||||
NTP *NTP
|
||||
DNS *DNS
|
||||
Experimental *Experimental
|
||||
|
@ -253,12 +262,18 @@ type RawTuicServer struct {
|
|||
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 {
|
||||
Port int `yaml:"port"`
|
||||
SocksPort int `yaml:"socks-port"`
|
||||
RedirPort int `yaml:"redir-port"`
|
||||
TProxyPort int `yaml:"tproxy-port"`
|
||||
MixedPort int `yaml:"mixed-port"`
|
||||
MitmPort int `yaml:"mitm-port"`
|
||||
ShadowSocksConfig string `yaml:"ss-config"`
|
||||
VmessConfig string `yaml:"vmess-config"`
|
||||
InboundTfo bool `yaml:"inbound-tfo"`
|
||||
|
@ -294,6 +309,7 @@ type RawConfig struct {
|
|||
TuicServer RawTuicServer `yaml:"tuic-server"`
|
||||
EBpf EBpf `yaml:"ebpf"`
|
||||
IPTables IPTables `yaml:"iptables"`
|
||||
MITM RawMitm `yaml:"mitm"`
|
||||
Experimental Experimental `yaml:"experimental"`
|
||||
Profile Profile `yaml:"profile"`
|
||||
GeoXUrl GeoXUrl `yaml:"geox-url"`
|
||||
|
@ -438,6 +454,10 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
|
|||
ParsePureIp: true,
|
||||
OverrideDest: true,
|
||||
},
|
||||
MITM: RawMitm{
|
||||
Port: 0,
|
||||
Rules: []string{},
|
||||
},
|
||||
Profile: Profile{
|
||||
StoreSelected: true,
|
||||
},
|
||||
|
@ -532,6 +552,12 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
mitm, err := parseMitm(rawCfg.MITM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Mitm = mitm
|
||||
|
||||
config.Users = parseAuthentication(rawCfg.Authentication)
|
||||
|
||||
config.Tunnels = rawCfg.Tunnels
|
||||
|
@ -582,6 +608,7 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
|
|||
RedirPort: cfg.RedirPort,
|
||||
TProxyPort: cfg.TProxyPort,
|
||||
MixedPort: cfg.MixedPort,
|
||||
MitmPort: cfg.MitmPort,
|
||||
ShadowSocksConfig: cfg.ShadowSocksConfig,
|
||||
VmessConfig: cfg.VmessConfig,
|
||||
AllowLan: cfg.AllowLan,
|
||||
|
@ -629,6 +656,11 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
|
|||
proxies["PASS"] = adapter.NewProxy(outbound.NewPass())
|
||||
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
|
||||
for idx, mapping := range proxiesConfig {
|
||||
proxy, err := adapter.ParseProxy(mapping)
|
||||
|
@ -909,6 +941,14 @@ func parseHosts(cfg *RawConfig) (*trie.DomainTrie[resolver.HostValue], error) {
|
|||
_ = 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()
|
||||
|
||||
return tree, nil
|
||||
|
@ -1457,3 +1497,28 @@ func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) {
|
|||
|
||||
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
|
||||
Reject
|
||||
Compatible
|
||||
Mitm
|
||||
Pass
|
||||
|
||||
Relay
|
||||
|
@ -182,6 +183,8 @@ func (at AdapterType) String() string {
|
|||
return "Compatible"
|
||||
case Pass:
|
||||
return "Pass"
|
||||
case Mitm:
|
||||
return "Mitm"
|
||||
case Shadowsocks:
|
||||
return "Shadowsocks"
|
||||
case ShadowsocksR:
|
||||
|
|
|
@ -31,6 +31,7 @@ const (
|
|||
TUN
|
||||
TUIC
|
||||
INNER
|
||||
MITM
|
||||
)
|
||||
|
||||
type NetWork int
|
||||
|
@ -80,6 +81,8 @@ func (t Type) String() string {
|
|||
return "Tuic"
|
||||
case INNER:
|
||||
return "Inner"
|
||||
case MITM:
|
||||
return "Mitm"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
|
@ -144,6 +147,8 @@ type Metadata struct {
|
|||
RemoteDst string `json:"remoteDestination"`
|
||||
// Only domain rule
|
||||
SniffHost string `json:"sniffHost"`
|
||||
// Only Mitm rule
|
||||
UserAgent string `json:"userAgent"`
|
||||
}
|
||||
|
||||
func (m *Metadata) RemoteAddress() string {
|
||||
|
|
|
@ -148,8 +148,12 @@ func (p *path) GeoSite() string {
|
|||
return P.Join(p.homeDir, "GeoSite.dat")
|
||||
}
|
||||
|
||||
func (p *path) GetAssetLocation(file string) string {
|
||||
return P.Join(p.homeDir, file)
|
||||
func (p *path) RootCA() string {
|
||||
return p.Resolve("mitm_ca.crt")
|
||||
}
|
||||
|
||||
func (p *path) CAKey() string {
|
||||
return p.Resolve("mitm_ca.key")
|
||||
}
|
||||
|
||||
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
|
||||
Uid
|
||||
SubRules
|
||||
UserAgent
|
||||
MATCH
|
||||
AND
|
||||
OR
|
||||
|
@ -67,6 +68,8 @@ func (rt RuleType) String() string {
|
|||
return "Process"
|
||||
case ProcessPath:
|
||||
return "ProcessPath"
|
||||
case UserAgent:
|
||||
return "UserAgent"
|
||||
case MATCH:
|
||||
return "Match"
|
||||
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/cors v1.2.1
|
||||
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/gorilla/websocket v1.5.0
|
||||
github.com/insomniacslk/dhcp v0.0.0-20230731140434-0f9eb93a696c
|
||||
|
@ -44,12 +45,14 @@ require (
|
|||
github.com/stretchr/testify v1.8.4
|
||||
github.com/zhangyunhao116/fastrand v0.3.0
|
||||
go.etcd.io/bbolt v1.3.7
|
||||
go.uber.org/atomic v1.9.0
|
||||
go.uber.org/automaxprocs v1.5.3
|
||||
golang.org/x/crypto v0.12.0
|
||||
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb
|
||||
golang.org/x/net v0.14.0
|
||||
golang.org/x/sync v0.3.0
|
||||
golang.org/x/sys v0.11.0
|
||||
golang.org/x/text v0.12.0
|
||||
google.golang.org/protobuf v1.31.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
lukechampine.com/blake3 v1.2.1
|
||||
|
@ -100,7 +103,6 @@ require (
|
|||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // 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/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-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/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/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
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=
|
||||
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
|
||||
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/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
|
||||
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)
|
||||
updateProxies(cfg.Proxies, cfg.Providers)
|
||||
updateProxies(cfg.Mitm, cfg.Proxies, cfg.Providers)
|
||||
updateRules(cfg.Rules, cfg.SubRules, cfg.RuleProviders)
|
||||
updateSniffer(cfg.Sniffer)
|
||||
updateHosts(cfg.Hosts)
|
||||
updateMitm(cfg.Mitm)
|
||||
updateGeneral(cfg.General)
|
||||
updateNTP(cfg.NTP)
|
||||
updateDNS(cfg.DNS, cfg.RuleProviders, cfg.General.IPv6)
|
||||
|
@ -134,6 +135,7 @@ func GetGeneral() *config.General {
|
|||
RedirPort: ports.RedirPort,
|
||||
TProxyPort: ports.TProxyPort,
|
||||
MixedPort: ports.MixedPort,
|
||||
MitmPort: ports.MitmPort,
|
||||
Tun: listener.GetTunConf(),
|
||||
TuicServer: listener.GetTuicConf(),
|
||||
ShadowSocksConfig: ports.ShadowSocksConfig,
|
||||
|
@ -262,7 +264,7 @@ func updateHosts(tree *trie.DomainTrie[resolver.HostValue]) {
|
|||
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)
|
||||
}
|
||||
|
||||
|
@ -490,6 +492,11 @@ func updateIPTables(cfg *config.Config) {
|
|||
log.Infoln("[IPTABLES] Setting iptables completed")
|
||||
}
|
||||
|
||||
func updateMitm(mitm *config.Mitm) {
|
||||
listener.ReCreateMitm(mitm.Port, tunnel.TCPIn())
|
||||
tunnel.UpdateRewrites(mitm.Rules)
|
||||
}
|
||||
|
||||
func Shutdown() {
|
||||
listener.Cleanup()
|
||||
tproxy.CleanupTProxyIPTables()
|
||||
|
|
|
@ -40,6 +40,7 @@ type configSchema struct {
|
|||
RedirPort *int `json:"redir-port"`
|
||||
TProxyPort *int `json:"tproxy-port"`
|
||||
MixedPort *int `json:"mixed-port"`
|
||||
MitmPort *int `json:"mitm-port"`
|
||||
Tun *tunSchema `json:"tun"`
|
||||
TuicServer *tuicServerSchema `json:"tuic-server"`
|
||||
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.ReCreateVmess(pointerOrDefaultString(general.VmessConfig, ports.VmessConfig), tcpIn, udpIn)
|
||||
P.ReCreateTuic(pointerOrDefaultTuicServer(general.TuicServer, P.LastTuicConf), tcpIn, udpIn)
|
||||
P.ReCreateMitm(pointerOrDefault(general.MitmPort, ports.MitmPort), tcpIn)
|
||||
|
||||
if general.Mode != nil {
|
||||
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
|
||||
|
||||
if !trusted {
|
||||
resp = authenticate(request, cache)
|
||||
resp = Authenticate(request, cache)
|
||||
|
||||
trusted = resp == nil
|
||||
}
|
||||
|
@ -66,19 +66,19 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.LruCache[strin
|
|||
return // hijack connection
|
||||
}
|
||||
|
||||
removeHopByHopHeaders(request.Header)
|
||||
removeExtraHTTPHostPort(request)
|
||||
RemoveHopByHopHeaders(request.Header)
|
||||
RemoveExtraHTTPHostPort(request)
|
||||
|
||||
if request.URL.Scheme == "" || request.URL.Host == "" {
|
||||
resp = responseWith(request, http.StatusBadRequest)
|
||||
resp = ResponseWith(request, http.StatusBadRequest)
|
||||
} else {
|
||||
resp, err = client.Do(request)
|
||||
if err != nil {
|
||||
resp = responseWith(request, http.StatusBadGateway)
|
||||
resp = ResponseWith(request, http.StatusBadGateway)
|
||||
}
|
||||
}
|
||||
|
||||
removeHopByHopHeaders(resp.Header)
|
||||
RemoveHopByHopHeaders(resp.Header)
|
||||
}
|
||||
|
||||
if keepAlive {
|
||||
|
@ -98,12 +98,12 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.LruCache[strin
|
|||
_ = 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()
|
||||
if authenticator != nil {
|
||||
credential := parseBasicProxyAuthorization(request)
|
||||
if credential == "" {
|
||||
resp := responseWith(request, http.StatusProxyAuthRequired)
|
||||
resp := ResponseWith(request, http.StatusProxyAuthRequired)
|
||||
resp.Header.Set("Proxy-Authenticate", "Basic")
|
||||
return resp
|
||||
}
|
||||
|
@ -117,14 +117,14 @@ func authenticate(request *http.Request, cache *cache.LruCache[string, bool]) *h
|
|||
if !authed {
|
||||
log.Infoln("Auth failed from %s", request.RemoteAddr)
|
||||
|
||||
return responseWith(request, http.StatusForbidden)
|
||||
return ResponseWith(request, http.StatusForbidden)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func responseWith(request *http.Request, statusCode int) *http.Response {
|
||||
func ResponseWith(request *http.Request, statusCode int) *http.Response {
|
||||
return &http.Response{
|
||||
StatusCode: statusCode,
|
||||
Status: http.StatusText(statusCode),
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/adapter/inbound"
|
||||
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()
|
||||
|
||||
removeProxyHeaders(request.Header)
|
||||
removeExtraHTTPHostPort(request)
|
||||
RemoveExtraHTTPHostPort(request)
|
||||
|
||||
address := request.Host
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// removeHopByHopHeaders remove hop-by-hop header
|
||||
func removeHopByHopHeaders(header http.Header) {
|
||||
// RemoveHopByHopHeaders remove hop-by-hop header
|
||||
func RemoveHopByHopHeaders(header http.Header) {
|
||||
// Strip hop-by-hop header based on RFC:
|
||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
|
||||
// https://www.mnot.net/blog/2011/07/11/what_proxies_must_do
|
||||
|
@ -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)
|
||||
func removeExtraHTTPHostPort(req *http.Request) {
|
||||
func RemoveExtraHTTPHostPort(req *http.Request) {
|
||||
host := req.Host
|
||||
if host == "" {
|
||||
host = req.URL.Host
|
||||
|
|
|
@ -1,19 +1,26 @@
|
|||
package listener
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"golang.org/x/exp/slices"
|
||||
"net"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/common/cert"
|
||||
"github.com/Dreamacro/clash/component/ebpf"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/listener/autoredir"
|
||||
LC "github.com/Dreamacro/clash/listener/config"
|
||||
"github.com/Dreamacro/clash/listener/http"
|
||||
"github.com/Dreamacro/clash/listener/mitm"
|
||||
"github.com/Dreamacro/clash/listener/mixed"
|
||||
"github.com/Dreamacro/clash/listener/redir"
|
||||
embedSS "github.com/Dreamacro/clash/listener/shadowsocks"
|
||||
|
@ -23,9 +30,10 @@ import (
|
|||
"github.com/Dreamacro/clash/listener/socks"
|
||||
"github.com/Dreamacro/clash/listener/tproxy"
|
||||
"github.com/Dreamacro/clash/listener/tuic"
|
||||
"github.com/Dreamacro/clash/listener/tunnel"
|
||||
LT "github.com/Dreamacro/clash/listener/tunnel"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
|
||||
rewrites "github.com/Dreamacro/clash/rewrite"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
|
@ -42,8 +50,8 @@ var (
|
|||
tproxyUDPListener *tproxy.UDPListener
|
||||
mixedListener *mixed.Listener
|
||||
mixedUDPLister *socks.UDPListener
|
||||
tunnelTCPListeners = map[string]*tunnel.Listener{}
|
||||
tunnelUDPListeners = map[string]*tunnel.PacketConn{}
|
||||
tunnelTCPListeners = map[string]*LT.Listener{}
|
||||
tunnelUDPListeners = map[string]*LT.PacketConn{}
|
||||
inboundListeners = map[string]C.InboundListener{}
|
||||
tunLister *sing_tun.Listener
|
||||
shadowSocksListener C.MultiAddrListener
|
||||
|
@ -52,6 +60,7 @@ var (
|
|||
autoRedirListener *autoredir.Listener
|
||||
autoRedirProgram *ebpf.TcEBpfProgram
|
||||
tcProgram *ebpf.TcEBpfProgram
|
||||
mitmListener *mitm.Listener
|
||||
|
||||
// lock for recreate function
|
||||
socksMux sync.Mutex
|
||||
|
@ -67,6 +76,7 @@ var (
|
|||
tuicMux sync.Mutex
|
||||
autoRedirMux sync.Mutex
|
||||
tcMux sync.Mutex
|
||||
mitmMux sync.Mutex
|
||||
|
||||
LastTunConf LC.Tun
|
||||
LastTuicConf LC.TuicServer
|
||||
|
@ -80,6 +90,7 @@ type Ports struct {
|
|||
MixedPort int `json:"mixed-port"`
|
||||
ShadowSocksConfig string `json:"ss-config"`
|
||||
VmessConfig string `json:"vmess-config"`
|
||||
MitmPort int `json:"mitm-port"`
|
||||
}
|
||||
|
||||
func GetTunConf() LC.Tun {
|
||||
|
@ -699,7 +710,7 @@ func PatchTunnel(tunnels []LC.Tunnel, tcpIn chan<- C.ConnContext, udpIn chan<- C
|
|||
for _, elm := range needCreate {
|
||||
key := fmt.Sprintf("%s/%s/%s", elm.addr, elm.target, elm.proxy)
|
||||
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 {
|
||||
log.Errorln("Start tunnel %s error: %s", elm.target, err.Error())
|
||||
continue
|
||||
|
@ -707,7 +718,7 @@ func PatchTunnel(tunnels []LC.Tunnel, tcpIn chan<- C.ConnContext, udpIn chan<- C
|
|||
tunnelTCPListeners[key] = l
|
||||
log.Infoln("Tunnel(tcp/%s) proxy %s listening at: %s", elm.target, elm.proxy, tunnelTCPListeners[key].Address())
|
||||
} 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 {
|
||||
log.Errorln("Start tunnel %s error: %s", elm.target, err.Error())
|
||||
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
|
||||
func GetPorts() *Ports {
|
||||
ports := &Ports{}
|
||||
|
@ -789,6 +873,12 @@ func GetPorts() *Ports {
|
|||
ports.VmessConfig = vmessListener.Config()
|
||||
}
|
||||
|
||||
if mitmListener != nil {
|
||||
_, portStr, _ := net.SplitHostPort(mitmListener.Address())
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
ports.MitmPort = port
|
||||
}
|
||||
|
||||
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() {
|
||||
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/u-root/uio v0.0.0-20230220225925-ffce2a382923 // 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/zhangyunhao116/fastrand v0.3.0 // indirect
|
||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // 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/exp v0.0.0-20230811145659-89c5cff77bcb // 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/time v0.3.0 // 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
|
||||
gopkg.in/yaml.v2 v2.4.0 // 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
|
||||
)
|
||||
|
|
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)
|
||||
natTable = nat.New()
|
||||
rules []C.Rule
|
||||
rewrites C.RewriteRule
|
||||
listeners = make(map[string]C.InboundListener)
|
||||
subRules map[string][]C.Rule
|
||||
proxies = make(map[string]C.Proxy)
|
||||
|
@ -179,6 +180,18 @@ func isHandle(t C.Type) bool {
|
|||
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
|
||||
func processUDP() {
|
||||
queue := udpQueue
|
||||
|
@ -433,8 +446,9 @@ func handleTCPConn(connCtx C.ConnContext) {
|
|||
return
|
||||
}
|
||||
|
||||
isMitmProxy := metadata.Type == C.MITM
|
||||
dialMetadata := metadata
|
||||
if len(metadata.Host) > 0 {
|
||||
if len(metadata.Host) > 0 && !isMitmProxy {
|
||||
if node, ok := resolver.DefaultHosts.Search(metadata.Host, false); ok {
|
||||
if dstIp, _ := node.RandIP(); !FakeIPRange().Contains(dstIp) {
|
||||
dialMetadata.DstIP = dstIp
|
||||
|
|
Loading…
Reference in New Issue
Block a user