Chore: merge branch 'with-tun' into plus-pro
This commit is contained in:
commit
05b4a326de
38
README.md
38
README.md
@ -36,12 +36,44 @@
|
||||
Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki).
|
||||
|
||||
## Advanced usage for this branch
|
||||
### MITM configuration
|
||||
A root CA certificate is required, the
|
||||
MITM proxy server will generate a CA certificate file and a CA private key file in your Clash home directory, you can use your own certificate replace it.
|
||||
|
||||
Need to install and trust the CA certificate on the client device, open this URL http://mitm.clash/cert.crt by the web browser to install the CA certificate, the host name 'mitm.clash' was always been hijacked.
|
||||
|
||||
NOTE: this feature cannot work on tls pinning
|
||||
|
||||
WARNING: DO NOT USE THIS FEATURE TO BREAK LOCAL LAWS
|
||||
|
||||
```yaml
|
||||
# Port of MITM proxy server on the local end
|
||||
mitm-port: 7894
|
||||
|
||||
# Man-In-The-Middle attack
|
||||
mitm:
|
||||
hosts: # use for others proxy type. E.g: TUN, socks
|
||||
- +.example.com
|
||||
rules: # rewrite rules
|
||||
- '^https?://www\.example\.com/1 url reject' # The "reject" returns HTTP status code 404 with no content.
|
||||
- '^https?://www\.example\.com/2 url reject-200' # The "reject-200" returns HTTP status code 200 with no content.
|
||||
- '^https?://www\.example\.com/3 url reject-img' # The "reject-img" returns HTTP status code 200 with content of 1px png.
|
||||
- '^https?://www\.example\.com/4 url reject-dict' # The "reject-dict" returns HTTP status code 200 with content of empty json object.
|
||||
- '^https?://www\.example\.com/5 url reject-array' # The "reject-array" returns HTTP status code 200 with content of empty json array.
|
||||
- '^https?://www\.example\.com/(6) url 302 https://www.example.com/new-$1'
|
||||
- '^https?://www\.(example)\.com/7 url 307 https://www.$1.com/new-7'
|
||||
- '^https?://www\.example\.com/8 url request-header (\r\n)User-Agent:.+(\r\n) request-header $1User-Agent: haha-wriohoh$2' # The "request-header" works for all the http headers not just one single header, so you can match two or more headers including CRLF in one regular expression.
|
||||
- '^https?://www\.example\.com/9 url request-body "pos_2":\[.*\],"pos_3" request-body "pos_2":[{"xx": "xx"}],"pos_3"'
|
||||
- '^https?://www\.example\.com/10 url response-header (\r\n)Tracecode:.+(\r\n) response-header $1Tracecode: 88888888888$2'
|
||||
- '^https?://www\.example\.com/11 url response-body "errmsg":"ok" response-body "errmsg":"not-ok"'
|
||||
```
|
||||
|
||||
### DNS configuration
|
||||
Support resolve ip with a proxy tunnel.
|
||||
|
||||
Support `geosite` with `fallback-filter`.
|
||||
|
||||
Use curl -X POST controllerip:port/cache/fakeip/flush to flush persistence fakeip
|
||||
Use `curl -X POST controllerip:port/cache/fakeip/flush` to flush persistence fakeip
|
||||
```yaml
|
||||
dns:
|
||||
enable: true
|
||||
@ -85,6 +117,7 @@ tun:
|
||||
```
|
||||
### Rules configuration
|
||||
- Support rule `GEOSITE`.
|
||||
- Support rule `USER-AGENT`.
|
||||
- Support `multiport` condition for rule `SRC-PORT` and `DST-PORT`.
|
||||
- Support `network` condition for all rules.
|
||||
- Support `process` condition for all rules.
|
||||
@ -116,6 +149,9 @@ rules:
|
||||
# multiport condition for rules SRC-PORT and DST-PORT
|
||||
- DST-PORT,123/136/137-139,DIRECT,udp
|
||||
|
||||
# USER-AGENT payload cannot include the comma character, '*' meaning any character.
|
||||
- USER-AGENT,*example*,PROXY
|
||||
|
||||
# rule GEOSITE
|
||||
- GEOSITE,category-ads-all,REJECT
|
||||
- GEOSITE,icloud@cn,DIRECT
|
||||
|
@ -3,11 +3,13 @@ package adapter
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
_ "unsafe"
|
||||
|
||||
"github.com/Dreamacro/clash/common/queue"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
@ -16,9 +18,12 @@ import (
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
//go:linkname errCanceled net.errCanceled
|
||||
var errCanceled error
|
||||
|
||||
type Proxy struct {
|
||||
C.ProxyAdapter
|
||||
history *queue.Queue
|
||||
history *queue.Queue[C.DelayHistory]
|
||||
alive *atomic.Bool
|
||||
}
|
||||
|
||||
@ -37,7 +42,7 @@ func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||
conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...)
|
||||
p.alive.Store(err == nil)
|
||||
p.alive.Store(err == nil || errors.Is(err, errCanceled))
|
||||
return conn, err
|
||||
}
|
||||
|
||||
@ -60,7 +65,7 @@ func (p *Proxy) DelayHistory() []C.DelayHistory {
|
||||
queue := p.history.Copy()
|
||||
histories := []C.DelayHistory{}
|
||||
for _, item := range queue {
|
||||
histories = append(histories, item.(C.DelayHistory))
|
||||
histories = append(histories, item)
|
||||
}
|
||||
return histories
|
||||
}
|
||||
@ -73,11 +78,7 @@ func (p *Proxy) LastDelay() (delay uint16) {
|
||||
return max
|
||||
}
|
||||
|
||||
last := p.history.Last()
|
||||
if last == nil {
|
||||
return max
|
||||
}
|
||||
history := last.(C.DelayHistory)
|
||||
history := p.history.Last()
|
||||
if history.Delay == 0 {
|
||||
return max
|
||||
}
|
||||
@ -161,7 +162,7 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
|
||||
}
|
||||
|
||||
func NewProxy(adapter C.ProxyAdapter) *Proxy {
|
||||
return &Proxy{adapter, queue.New(10), atomic.NewBool(true)}
|
||||
return &Proxy{adapter, queue.New[C.DelayHistory](10), atomic.NewBool(true)}
|
||||
}
|
||||
|
||||
func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
|
||||
|
22
adapter/inbound/mitm.go
Normal file
22
adapter/inbound/mitm.go
Normal file
@ -0,0 +1,22 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/context"
|
||||
"github.com/Dreamacro/clash/transport/socks5"
|
||||
)
|
||||
|
||||
// NewMitm receive mitm request and return MitmContext
|
||||
func NewMitm(target socks5.Addr, source net.Addr, userAgent string, conn net.Conn) *context.ConnContext {
|
||||
metadata := parseSocksAddr(target)
|
||||
metadata.NetWork = C.TCP
|
||||
metadata.Type = C.MITM
|
||||
metadata.UserAgent = userAgent
|
||||
if ip, port, err := parseAddr(source.String()); err == nil {
|
||||
metadata.SrcIP = ip
|
||||
metadata.SrcPort = port
|
||||
}
|
||||
return context.NewConnContext(conn, metadata)
|
||||
}
|
@ -89,6 +89,10 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
|
||||
req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
|
||||
}
|
||||
|
||||
if metadata.Type == C.MITM {
|
||||
req.Header.Add("Origin-Request-Source-Address", metadata.SourceAddress())
|
||||
}
|
||||
|
||||
if err := req.Write(rw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
68
adapter/outbound/mitm.go
Normal file
68
adapter/outbound/mitm.go
Normal file
@ -0,0 +1,68 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
"github.com/Dreamacro/clash/component/trie"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
var (
|
||||
errIgnored = errors.New("not match in mitm host lists")
|
||||
httpProxyClient = NewHttp(HttpOption{})
|
||||
|
||||
MiddlemanServerAddress = atomic.NewString("")
|
||||
MiddlemanRewriteHosts *trie.DomainTrie[bool]
|
||||
)
|
||||
|
||||
type Mitm struct {
|
||||
*Base
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (d *Mitm) DialContext(ctx context.Context, metadata *C.Metadata, _ ...dialer.Option) (C.Conn, error) {
|
||||
addr := MiddlemanServerAddress.Load()
|
||||
if addr == "" || MiddlemanRewriteHosts == nil {
|
||||
return nil, errIgnored
|
||||
}
|
||||
|
||||
if MiddlemanRewriteHosts.Search(metadata.String()) == nil {
|
||||
return nil, errIgnored
|
||||
}
|
||||
|
||||
metadata.Type = C.MITM
|
||||
|
||||
if metadata.Host != "" {
|
||||
metadata.AddrType = C.AtypDomainName
|
||||
metadata.DstIP = nil
|
||||
}
|
||||
|
||||
c, err := dialer.DialContext(ctx, "tcp", addr, []dialer.Option{dialer.WithInterface(""), dialer.WithRoutingMark(0)}...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tcpKeepAlive(c)
|
||||
|
||||
defer safeConnClose(c, err)
|
||||
|
||||
c, err = httpProxyClient.StreamConn(c, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewConn(c, d), nil
|
||||
}
|
||||
|
||||
func NewMitm() *Mitm {
|
||||
return &Mitm{
|
||||
Base: &Base{
|
||||
name: "Mitm",
|
||||
tp: C.Mitm,
|
||||
},
|
||||
}
|
||||
}
|
48
common/cache/cache.go
vendored
48
common/cache/cache.go
vendored
@ -7,50 +7,50 @@ import (
|
||||
)
|
||||
|
||||
// Cache store element with a expired time
|
||||
type Cache struct {
|
||||
*cache
|
||||
type Cache[K comparable, V any] struct {
|
||||
*cache[K, V]
|
||||
}
|
||||
|
||||
type cache struct {
|
||||
type cache[K comparable, V any] struct {
|
||||
mapping sync.Map
|
||||
janitor *janitor
|
||||
janitor *janitor[K, V]
|
||||
}
|
||||
|
||||
type element struct {
|
||||
type element[V any] struct {
|
||||
Expired time.Time
|
||||
Payload any
|
||||
Payload V
|
||||
}
|
||||
|
||||
// Put element in Cache with its ttl
|
||||
func (c *cache) Put(key any, payload any, ttl time.Duration) {
|
||||
c.mapping.Store(key, &element{
|
||||
func (c *cache[K, V]) Put(key K, payload V, ttl time.Duration) {
|
||||
c.mapping.Store(key, &element[V]{
|
||||
Payload: payload,
|
||||
Expired: time.Now().Add(ttl),
|
||||
})
|
||||
}
|
||||
|
||||
// Get element in Cache, and drop when it expired
|
||||
func (c *cache) Get(key any) any {
|
||||
func (c *cache[K, V]) Get(key K) V {
|
||||
item, exist := c.mapping.Load(key)
|
||||
if !exist {
|
||||
return nil
|
||||
return getZero[V]()
|
||||
}
|
||||
elm := item.(*element)
|
||||
elm := item.(*element[V])
|
||||
// expired
|
||||
if time.Since(elm.Expired) > 0 {
|
||||
c.mapping.Delete(key)
|
||||
return nil
|
||||
return getZero[V]()
|
||||
}
|
||||
return elm.Payload
|
||||
}
|
||||
|
||||
// GetWithExpire element in Cache with Expire Time
|
||||
func (c *cache) GetWithExpire(key any) (payload any, expired time.Time) {
|
||||
func (c *cache[K, V]) GetWithExpire(key K) (payload V, expired time.Time) {
|
||||
item, exist := c.mapping.Load(key)
|
||||
if !exist {
|
||||
return
|
||||
}
|
||||
elm := item.(*element)
|
||||
elm := item.(*element[V])
|
||||
// expired
|
||||
if time.Since(elm.Expired) > 0 {
|
||||
c.mapping.Delete(key)
|
||||
@ -59,10 +59,10 @@ func (c *cache) GetWithExpire(key any) (payload any, expired time.Time) {
|
||||
return elm.Payload, elm.Expired
|
||||
}
|
||||
|
||||
func (c *cache) cleanup() {
|
||||
func (c *cache[K, V]) cleanup() {
|
||||
c.mapping.Range(func(k, v any) bool {
|
||||
key := k.(string)
|
||||
elm := v.(*element)
|
||||
elm := v.(*element[V])
|
||||
if time.Since(elm.Expired) > 0 {
|
||||
c.mapping.Delete(key)
|
||||
}
|
||||
@ -70,12 +70,12 @@ func (c *cache) cleanup() {
|
||||
})
|
||||
}
|
||||
|
||||
type janitor struct {
|
||||
type janitor[K comparable, V any] struct {
|
||||
interval time.Duration
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
func (j *janitor) process(c *cache) {
|
||||
func (j *janitor[K, V]) process(c *cache[K, V]) {
|
||||
ticker := time.NewTicker(j.interval)
|
||||
for {
|
||||
select {
|
||||
@ -88,19 +88,19 @@ func (j *janitor) process(c *cache) {
|
||||
}
|
||||
}
|
||||
|
||||
func stopJanitor(c *Cache) {
|
||||
func stopJanitor[K comparable, V any](c *Cache[K, V]) {
|
||||
c.janitor.stop <- struct{}{}
|
||||
}
|
||||
|
||||
// New return *Cache
|
||||
func New(interval time.Duration) *Cache {
|
||||
j := &janitor{
|
||||
func New[K comparable, V any](interval time.Duration) *Cache[K, V] {
|
||||
j := &janitor[K, V]{
|
||||
interval: interval,
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
c := &cache{janitor: j}
|
||||
c := &cache[K, V]{janitor: j}
|
||||
go j.process(c)
|
||||
C := &Cache{c}
|
||||
runtime.SetFinalizer(C, stopJanitor)
|
||||
C := &Cache[K, V]{c}
|
||||
runtime.SetFinalizer(C, stopJanitor[K, V])
|
||||
return C
|
||||
}
|
||||
|
28
common/cache/cache_test.go
vendored
28
common/cache/cache_test.go
vendored
@ -11,48 +11,50 @@ import (
|
||||
func TestCache_Basic(t *testing.T) {
|
||||
interval := 200 * time.Millisecond
|
||||
ttl := 20 * time.Millisecond
|
||||
c := New(interval)
|
||||
c := New[string, int](interval)
|
||||
c.Put("int", 1, ttl)
|
||||
c.Put("string", "a", ttl)
|
||||
|
||||
d := New[string, string](interval)
|
||||
d.Put("string", "a", ttl)
|
||||
|
||||
i := c.Get("int")
|
||||
assert.Equal(t, i.(int), 1, "should recv 1")
|
||||
assert.Equal(t, i, 1, "should recv 1")
|
||||
|
||||
s := c.Get("string")
|
||||
assert.Equal(t, s.(string), "a", "should recv 'a'")
|
||||
s := d.Get("string")
|
||||
assert.Equal(t, s, "a", "should recv 'a'")
|
||||
}
|
||||
|
||||
func TestCache_TTL(t *testing.T) {
|
||||
interval := 200 * time.Millisecond
|
||||
ttl := 20 * time.Millisecond
|
||||
now := time.Now()
|
||||
c := New(interval)
|
||||
c := New[string, int](interval)
|
||||
c.Put("int", 1, ttl)
|
||||
c.Put("int2", 2, ttl)
|
||||
|
||||
i := c.Get("int")
|
||||
_, expired := c.GetWithExpire("int2")
|
||||
assert.Equal(t, i.(int), 1, "should recv 1")
|
||||
assert.Equal(t, i, 1, "should recv 1")
|
||||
assert.True(t, now.Before(expired))
|
||||
|
||||
time.Sleep(ttl * 2)
|
||||
i = c.Get("int")
|
||||
j, _ := c.GetWithExpire("int2")
|
||||
assert.Nil(t, i, "should recv nil")
|
||||
assert.Nil(t, j, "should recv nil")
|
||||
assert.True(t, i == 0, "should recv 0")
|
||||
assert.True(t, j == 0, "should recv 0")
|
||||
}
|
||||
|
||||
func TestCache_AutoCleanup(t *testing.T) {
|
||||
interval := 10 * time.Millisecond
|
||||
ttl := 15 * time.Millisecond
|
||||
c := New(interval)
|
||||
c := New[string, int](interval)
|
||||
c.Put("int", 1, ttl)
|
||||
|
||||
time.Sleep(ttl * 2)
|
||||
i := c.Get("int")
|
||||
j, _ := c.GetWithExpire("int")
|
||||
assert.Nil(t, i, "should recv nil")
|
||||
assert.Nil(t, j, "should recv nil")
|
||||
assert.True(t, i == 0, "should recv 0")
|
||||
assert.True(t, j == 0, "should recv 0")
|
||||
}
|
||||
|
||||
func TestCache_AutoGC(t *testing.T) {
|
||||
@ -60,7 +62,7 @@ func TestCache_AutoGC(t *testing.T) {
|
||||
go func() {
|
||||
interval := 10 * time.Millisecond
|
||||
ttl := 15 * time.Millisecond
|
||||
c := New(interval)
|
||||
c := New[string, int](interval)
|
||||
c.Put("int", 1, ttl)
|
||||
sign <- struct{}{}
|
||||
}()
|
||||
|
97
common/cache/lrucache.go
vendored
97
common/cache/lrucache.go
vendored
@ -9,43 +9,43 @@ import (
|
||||
)
|
||||
|
||||
// Option is part of Functional Options Pattern
|
||||
type Option func(*LruCache)
|
||||
type Option[K comparable, V any] func(*LruCache[K, V])
|
||||
|
||||
// EvictCallback is used to get a callback when a cache entry is evicted
|
||||
type EvictCallback = func(key any, value any)
|
||||
|
||||
// WithEvict set the evict callback
|
||||
func WithEvict(cb EvictCallback) Option {
|
||||
return func(l *LruCache) {
|
||||
func WithEvict[K comparable, V any](cb EvictCallback) Option[K, V] {
|
||||
return func(l *LruCache[K, V]) {
|
||||
l.onEvict = cb
|
||||
}
|
||||
}
|
||||
|
||||
// WithUpdateAgeOnGet update expires when Get element
|
||||
func WithUpdateAgeOnGet() Option {
|
||||
return func(l *LruCache) {
|
||||
func WithUpdateAgeOnGet[K comparable, V any]() Option[K, V] {
|
||||
return func(l *LruCache[K, V]) {
|
||||
l.updateAgeOnGet = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithAge defined element max age (second)
|
||||
func WithAge(maxAge int64) Option {
|
||||
return func(l *LruCache) {
|
||||
func WithAge[K comparable, V any](maxAge int64) Option[K, V] {
|
||||
return func(l *LruCache[K, V]) {
|
||||
l.maxAge = maxAge
|
||||
}
|
||||
}
|
||||
|
||||
// WithSize defined max length of LruCache
|
||||
func WithSize(maxSize int) Option {
|
||||
return func(l *LruCache) {
|
||||
func WithSize[K comparable, V any](maxSize int) Option[K, V] {
|
||||
return func(l *LruCache[K, V]) {
|
||||
l.maxSize = maxSize
|
||||
}
|
||||
}
|
||||
|
||||
// WithStale decide whether Stale return is enabled.
|
||||
// If this feature is enabled, element will not get Evicted according to `WithAge`.
|
||||
func WithStale(stale bool) Option {
|
||||
return func(l *LruCache) {
|
||||
func WithStale[K comparable, V any](stale bool) Option[K, V] {
|
||||
return func(l *LruCache[K, V]) {
|
||||
l.staleReturn = stale
|
||||
}
|
||||
}
|
||||
@ -53,7 +53,7 @@ func WithStale(stale bool) Option {
|
||||
// LruCache is a thread-safe, in-memory lru-cache that evicts the
|
||||
// least recently used entries from memory when (if set) the entries are
|
||||
// older than maxAge (in seconds). Use the New constructor to create one.
|
||||
type LruCache struct {
|
||||
type LruCache[K comparable, V any] struct {
|
||||
maxAge int64
|
||||
maxSize int
|
||||
mu sync.Mutex
|
||||
@ -65,8 +65,8 @@ type LruCache struct {
|
||||
}
|
||||
|
||||
// NewLRUCache creates an LruCache
|
||||
func NewLRUCache(options ...Option) *LruCache {
|
||||
lc := &LruCache{
|
||||
func NewLRUCache[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] {
|
||||
lc := &LruCache[K, V]{
|
||||
lru: list.New(),
|
||||
cache: make(map[any]*list.Element),
|
||||
}
|
||||
@ -80,12 +80,12 @@ func NewLRUCache(options ...Option) *LruCache {
|
||||
|
||||
// Get returns the any representation of a cached response and a bool
|
||||
// set to true if the key was found.
|
||||
func (c *LruCache) Get(key any) (any, bool) {
|
||||
entry := c.get(key)
|
||||
if entry == nil {
|
||||
return nil, false
|
||||
func (c *LruCache[K, V]) Get(key K) (V, bool) {
|
||||
el := c.get(key)
|
||||
if el == nil {
|
||||
return getZero[V](), false
|
||||
}
|
||||
value := entry.value
|
||||
value := el.value
|
||||
|
||||
return value, true
|
||||
}
|
||||
@ -94,17 +94,17 @@ func (c *LruCache) Get(key any) (any, bool) {
|
||||
// a time.Time Give expected expires,
|
||||
// and a bool set to true if the key was found.
|
||||
// This method will NOT check the maxAge of element and will NOT update the expires.
|
||||
func (c *LruCache) GetWithExpire(key any) (any, time.Time, bool) {
|
||||
entry := c.get(key)
|
||||
if entry == nil {
|
||||
return nil, time.Time{}, false
|
||||
func (c *LruCache[K, V]) GetWithExpire(key K) (V, time.Time, bool) {
|
||||
el := c.get(key)
|
||||
if el == nil {
|
||||
return getZero[V](), time.Time{}, false
|
||||
}
|
||||
|
||||
return entry.value, time.Unix(entry.expires, 0), true
|
||||
return el.value, time.Unix(el.expires, 0), true
|
||||
}
|
||||
|
||||
// Exist returns if key exist in cache but not put item to the head of linked list
|
||||
func (c *LruCache) Exist(key any) bool {
|
||||
func (c *LruCache[K, V]) Exist(key K) bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@ -113,7 +113,7 @@ func (c *LruCache) Exist(key any) bool {
|
||||
}
|
||||
|
||||
// Set stores the any representation of a response for a given key.
|
||||
func (c *LruCache) Set(key any, value any) {
|
||||
func (c *LruCache[K, V]) Set(key K, value V) {
|
||||
expires := int64(0)
|
||||
if c.maxAge > 0 {
|
||||
expires = time.Now().Unix() + c.maxAge
|
||||
@ -123,21 +123,21 @@ func (c *LruCache) Set(key any, value any) {
|
||||
|
||||
// SetWithExpire stores the any representation of a response for a given key and given expires.
|
||||
// The expires time will round to second.
|
||||
func (c *LruCache) SetWithExpire(key any, value any, expires time.Time) {
|
||||
func (c *LruCache[K, V]) SetWithExpire(key K, value V, expires time.Time) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if le, ok := c.cache[key]; ok {
|
||||
c.lru.MoveToBack(le)
|
||||
e := le.Value.(*entry)
|
||||
e := le.Value.(*entry[K, V])
|
||||
e.value = value
|
||||
e.expires = expires.Unix()
|
||||
} else {
|
||||
e := &entry{key: key, value: value, expires: expires.Unix()}
|
||||
e := &entry[K, V]{key: key, value: value, expires: expires.Unix()}
|
||||
c.cache[key] = c.lru.PushBack(e)
|
||||
|
||||
if c.maxSize > 0 {
|
||||
if len := c.lru.Len(); len > c.maxSize {
|
||||
if elLen := c.lru.Len(); elLen > c.maxSize {
|
||||
c.deleteElement(c.lru.Front())
|
||||
}
|
||||
}
|
||||
@ -147,7 +147,7 @@ func (c *LruCache) SetWithExpire(key any, value any, expires time.Time) {
|
||||
}
|
||||
|
||||
// CloneTo clone and overwrite elements to another LruCache
|
||||
func (c *LruCache) CloneTo(n *LruCache) {
|
||||
func (c *LruCache[K, V]) CloneTo(n *LruCache[K, V]) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@ -158,12 +158,12 @@ func (c *LruCache) CloneTo(n *LruCache) {
|
||||
n.cache = make(map[any]*list.Element)
|
||||
|
||||
for e := c.lru.Front(); e != nil; e = e.Next() {
|
||||
elm := e.Value.(*entry)
|
||||
elm := e.Value.(*entry[K, V])
|
||||
n.cache[elm.key] = n.lru.PushBack(elm)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *LruCache) get(key any) *entry {
|
||||
func (c *LruCache[K, V]) get(key K) *entry[K, V] {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@ -172,7 +172,7 @@ func (c *LruCache) get(key any) *entry {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !c.staleReturn && c.maxAge > 0 && le.Value.(*entry).expires <= time.Now().Unix() {
|
||||
if !c.staleReturn && c.maxAge > 0 && le.Value.(*entry[K, V]).expires <= time.Now().Unix() {
|
||||
c.deleteElement(le)
|
||||
c.maybeDeleteOldest()
|
||||
|
||||
@ -180,15 +180,15 @@ func (c *LruCache) get(key any) *entry {
|
||||
}
|
||||
|
||||
c.lru.MoveToBack(le)
|
||||
entry := le.Value.(*entry)
|
||||
el := le.Value.(*entry[K, V])
|
||||
if c.maxAge > 0 && c.updateAgeOnGet {
|
||||
entry.expires = time.Now().Unix() + c.maxAge
|
||||
el.expires = time.Now().Unix() + c.maxAge
|
||||
}
|
||||
return entry
|
||||
return el
|
||||
}
|
||||
|
||||
// Delete removes the value associated with a key.
|
||||
func (c *LruCache) Delete(key any) {
|
||||
func (c *LruCache[K, V]) Delete(key K) {
|
||||
c.mu.Lock()
|
||||
|
||||
if le, ok := c.cache[key]; ok {
|
||||
@ -198,25 +198,25 @@ func (c *LruCache) Delete(key any) {
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *LruCache) maybeDeleteOldest() {
|
||||
func (c *LruCache[K, V]) maybeDeleteOldest() {
|
||||
if !c.staleReturn && c.maxAge > 0 {
|
||||
now := time.Now().Unix()
|
||||
for le := c.lru.Front(); le != nil && le.Value.(*entry).expires <= now; le = c.lru.Front() {
|
||||
for le := c.lru.Front(); le != nil && le.Value.(*entry[K, V]).expires <= now; le = c.lru.Front() {
|
||||
c.deleteElement(le)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *LruCache) deleteElement(le *list.Element) {
|
||||
func (c *LruCache[K, V]) deleteElement(le *list.Element) {
|
||||
c.lru.Remove(le)
|
||||
e := le.Value.(*entry)
|
||||
e := le.Value.(*entry[K, V])
|
||||
delete(c.cache, e.key)
|
||||
if c.onEvict != nil {
|
||||
c.onEvict(e.key, e.value)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *LruCache) Clear() error {
|
||||
func (c *LruCache[K, V]) Clear() error {
|
||||
c.mu.Lock()
|
||||
|
||||
c.cache = make(map[any]*list.Element)
|
||||
@ -225,8 +225,13 @@ func (c *LruCache) Clear() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
key any
|
||||
value any
|
||||
type entry[K comparable, V any] struct {
|
||||
key K
|
||||
value V
|
||||
expires int64
|
||||
}
|
||||
|
||||
func getZero[T any]() T {
|
||||
var result T
|
||||
return result
|
||||
}
|
||||
|
41
common/cache/lrucache_test.go
vendored
41
common/cache/lrucache_test.go
vendored
@ -19,7 +19,7 @@ var entries = []struct {
|
||||
}
|
||||
|
||||
func TestLRUCache(t *testing.T) {
|
||||
c := NewLRUCache()
|
||||
c := NewLRUCache[string, string]()
|
||||
|
||||
for _, e := range entries {
|
||||
c.Set(e.key, e.value)
|
||||
@ -32,7 +32,7 @@ func TestLRUCache(t *testing.T) {
|
||||
for _, e := range entries {
|
||||
value, ok := c.Get(e.key)
|
||||
if assert.True(t, ok) {
|
||||
assert.Equal(t, e.value, value.(string))
|
||||
assert.Equal(t, e.value, value)
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,25 +45,25 @@ func TestLRUCache(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLRUMaxAge(t *testing.T) {
|
||||
c := NewLRUCache(WithAge(86400))
|
||||
c := NewLRUCache[string, string](WithAge[string, string](86400))
|
||||
|
||||
now := time.Now().Unix()
|
||||
expected := now + 86400
|
||||
|
||||
// Add one expired entry
|
||||
c.Set("foo", "bar")
|
||||
c.lru.Back().Value.(*entry).expires = now
|
||||
c.lru.Back().Value.(*entry[string, string]).expires = now
|
||||
|
||||
// Reset
|
||||
c.Set("foo", "bar")
|
||||
e := c.lru.Back().Value.(*entry)
|
||||
e := c.lru.Back().Value.(*entry[string, string])
|
||||
assert.True(t, e.expires >= now)
|
||||
c.lru.Back().Value.(*entry).expires = now
|
||||
c.lru.Back().Value.(*entry[string, string]).expires = now
|
||||
|
||||
// Set a few and verify expiration times
|
||||
for _, s := range entries {
|
||||
c.Set(s.key, s.value)
|
||||
e := c.lru.Back().Value.(*entry)
|
||||
e := c.lru.Back().Value.(*entry[string, string])
|
||||
assert.True(t, e.expires >= expected && e.expires <= expected+10)
|
||||
}
|
||||
|
||||
@ -77,7 +77,7 @@ func TestLRUMaxAge(t *testing.T) {
|
||||
for _, s := range entries {
|
||||
le, ok := c.cache[s.key]
|
||||
if assert.True(t, ok) {
|
||||
le.Value.(*entry).expires = now
|
||||
le.Value.(*entry[string, string]).expires = now
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,22 +88,22 @@ func TestLRUMaxAge(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLRUpdateOnGet(t *testing.T) {
|
||||
c := NewLRUCache(WithAge(86400), WithUpdateAgeOnGet())
|
||||
c := NewLRUCache[string, string](WithAge[string, string](86400), WithUpdateAgeOnGet[string, string]())
|
||||
|
||||
now := time.Now().Unix()
|
||||
expires := now + 86400/2
|
||||
|
||||
// Add one expired entry
|
||||
c.Set("foo", "bar")
|
||||
c.lru.Back().Value.(*entry).expires = expires
|
||||
c.lru.Back().Value.(*entry[string, string]).expires = expires
|
||||
|
||||
_, ok := c.Get("foo")
|
||||
assert.True(t, ok)
|
||||
assert.True(t, c.lru.Back().Value.(*entry).expires > expires)
|
||||
assert.True(t, c.lru.Back().Value.(*entry[string, string]).expires > expires)
|
||||
}
|
||||
|
||||
func TestMaxSize(t *testing.T) {
|
||||
c := NewLRUCache(WithSize(2))
|
||||
c := NewLRUCache[string, string](WithSize[string, string](2))
|
||||
// Add one expired entry
|
||||
c.Set("foo", "bar")
|
||||
_, ok := c.Get("foo")
|
||||
@ -117,7 +117,7 @@ func TestMaxSize(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestExist(t *testing.T) {
|
||||
c := NewLRUCache(WithSize(1))
|
||||
c := NewLRUCache[int, int](WithSize[int, int](1))
|
||||
c.Set(1, 2)
|
||||
assert.True(t, c.Exist(1))
|
||||
c.Set(2, 3)
|
||||
@ -130,7 +130,7 @@ func TestEvict(t *testing.T) {
|
||||
temp = key.(int) + value.(int)
|
||||
}
|
||||
|
||||
c := NewLRUCache(WithEvict(evict), WithSize(1))
|
||||
c := NewLRUCache[int, int](WithEvict[int, int](evict), WithSize[int, int](1))
|
||||
c.Set(1, 2)
|
||||
c.Set(2, 3)
|
||||
|
||||
@ -138,21 +138,22 @@ func TestEvict(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSetWithExpire(t *testing.T) {
|
||||
c := NewLRUCache(WithAge(1))
|
||||
c := NewLRUCache[int, *struct{}](WithAge[int, *struct{}](1))
|
||||
now := time.Now().Unix()
|
||||
|
||||
tenSecBefore := time.Unix(now-10, 0)
|
||||
c.SetWithExpire(1, 2, tenSecBefore)
|
||||
c.SetWithExpire(1, &struct{}{}, tenSecBefore)
|
||||
|
||||
// res is expected not to exist, and expires should be empty time.Time
|
||||
res, expires, exist := c.GetWithExpire(1)
|
||||
assert.Equal(t, nil, res)
|
||||
|
||||
assert.True(t, nil == res)
|
||||
assert.Equal(t, time.Time{}, expires)
|
||||
assert.Equal(t, false, exist)
|
||||
}
|
||||
|
||||
func TestStale(t *testing.T) {
|
||||
c := NewLRUCache(WithAge(1), WithStale(true))
|
||||
c := NewLRUCache[int, int](WithAge[int, int](1), WithStale[int, int](true))
|
||||
now := time.Now().Unix()
|
||||
|
||||
tenSecBefore := time.Unix(now-10, 0)
|
||||
@ -165,11 +166,11 @@ func TestStale(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCloneTo(t *testing.T) {
|
||||
o := NewLRUCache(WithSize(10))
|
||||
o := NewLRUCache[string, int](WithSize[string, int](10))
|
||||
o.Set("1", 1)
|
||||
o.Set("2", 2)
|
||||
|
||||
n := NewLRUCache(WithSize(2))
|
||||
n := NewLRUCache[string, int](WithSize[string, int](2))
|
||||
n.Set("3", 3)
|
||||
n.Set("4", 4)
|
||||
|
||||
|
282
common/cert/cert.go
Normal file
282
common/cert/cert.go
Normal file
@ -0,0 +1,282 @@
|
||||
package cert
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
var currentSerialNumber = time.Now().Unix()
|
||||
|
||||
type Config struct {
|
||||
ca *x509.Certificate
|
||||
caPrivateKey *rsa.PrivateKey
|
||||
|
||||
roots *x509.CertPool
|
||||
|
||||
privateKey *rsa.PrivateKey
|
||||
|
||||
validity time.Duration
|
||||
keyID []byte
|
||||
organization string
|
||||
|
||||
certsStorage CertsStorage
|
||||
}
|
||||
|
||||
type CertsStorage interface {
|
||||
Get(key string) (*tls.Certificate, bool)
|
||||
|
||||
Set(key string, cert *tls.Certificate)
|
||||
}
|
||||
|
||||
type CertsCache struct {
|
||||
certsCache map[string]*tls.Certificate
|
||||
}
|
||||
|
||||
func (c *CertsCache) Get(key string) (*tls.Certificate, bool) {
|
||||
v, ok := c.certsCache[key]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (c *CertsCache) Set(key string, cert *tls.Certificate) {
|
||||
c.certsCache[key] = cert
|
||||
}
|
||||
|
||||
func NewAuthority(name, organization string, validity time.Duration) (*x509.Certificate, *rsa.PrivateKey, error) {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
pub := privateKey.Public()
|
||||
|
||||
pkixPub, err := x509.MarshalPKIXPublicKey(pub)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
h := sha1.New()
|
||||
_, err = h.Write(pkixPub)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
keyID := h.Sum(nil)
|
||||
|
||||
serial := atomic.AddInt64(¤tSerialNumber, 1)
|
||||
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(serial),
|
||||
Subject: pkix.Name{
|
||||
CommonName: name,
|
||||
Organization: []string{organization},
|
||||
},
|
||||
SubjectKeyId: keyID,
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
NotBefore: time.Now().Add(-validity),
|
||||
NotAfter: time.Now().Add(validity),
|
||||
DNSNames: []string{name},
|
||||
IsCA: true,
|
||||
}
|
||||
|
||||
raw, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, pub, privateKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
x509c, err := x509.ParseCertificate(raw)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return x509c, privateKey, nil
|
||||
}
|
||||
|
||||
func NewConfig(ca *x509.Certificate, caPrivateKey *rsa.PrivateKey, storage CertsStorage) (*Config, error) {
|
||||
roots := x509.NewCertPool()
|
||||
roots.AddCert(ca)
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pub := privateKey.Public()
|
||||
|
||||
pkixPub, err := x509.MarshalPKIXPublicKey(pub)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h := sha1.New()
|
||||
_, err = h.Write(pkixPub)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyID := h.Sum(nil)
|
||||
|
||||
if storage == nil {
|
||||
storage = &CertsCache{certsCache: make(map[string]*tls.Certificate)}
|
||||
}
|
||||
|
||||
return &Config{
|
||||
ca: ca,
|
||||
caPrivateKey: caPrivateKey,
|
||||
privateKey: privateKey,
|
||||
keyID: keyID,
|
||||
validity: time.Hour,
|
||||
organization: "Clash",
|
||||
certsStorage: storage,
|
||||
roots: roots,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Config) GetCA() *x509.Certificate {
|
||||
return c.ca
|
||||
}
|
||||
|
||||
func (c *Config) SetOrganization(organization string) {
|
||||
c.organization = organization
|
||||
}
|
||||
|
||||
func (c *Config) SetValidity(validity time.Duration) {
|
||||
c.validity = validity
|
||||
}
|
||||
|
||||
func (c *Config) NewTLSConfigForHost(hostname string) *tls.Config {
|
||||
tlsConfig := &tls.Config{
|
||||
GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
host := clientHello.ServerName
|
||||
if host == "" {
|
||||
host = hostname
|
||||
}
|
||||
|
||||
return c.GetOrCreateCert(host)
|
||||
},
|
||||
NextProtos: []string{"http/1.1"},
|
||||
}
|
||||
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
|
||||
return tlsConfig
|
||||
}
|
||||
|
||||
func (c *Config) GetOrCreateCert(hostname string, ips ...net.IP) (*tls.Certificate, error) {
|
||||
host, _, err := net.SplitHostPort(hostname)
|
||||
if err == nil {
|
||||
hostname = host
|
||||
}
|
||||
|
||||
tlsCertificate, ok := c.certsStorage.Get(hostname)
|
||||
if ok {
|
||||
if _, err = tlsCertificate.Leaf.Verify(x509.VerifyOptions{
|
||||
DNSName: hostname,
|
||||
Roots: c.roots,
|
||||
}); err == nil {
|
||||
return tlsCertificate, nil
|
||||
}
|
||||
}
|
||||
|
||||
serial := atomic.AddInt64(¤tSerialNumber, 1)
|
||||
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(serial),
|
||||
Subject: pkix.Name{
|
||||
CommonName: hostname,
|
||||
Organization: []string{c.organization},
|
||||
},
|
||||
SubjectKeyId: c.keyID,
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
NotBefore: time.Now().Add(-c.validity),
|
||||
NotAfter: time.Now().Add(c.validity),
|
||||
}
|
||||
|
||||
if ip := net.ParseIP(hostname); ip != nil {
|
||||
ips = append(ips, ip)
|
||||
} else {
|
||||
tmpl.DNSNames = []string{hostname}
|
||||
}
|
||||
|
||||
tmpl.IPAddresses = ips
|
||||
|
||||
raw, err := x509.CreateCertificate(rand.Reader, tmpl, c.ca, c.privateKey.Public(), c.caPrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
x509c, err := x509.ParseCertificate(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tlsCertificate = &tls.Certificate{
|
||||
Certificate: [][]byte{raw, c.ca.Raw},
|
||||
PrivateKey: c.privateKey,
|
||||
Leaf: x509c,
|
||||
}
|
||||
|
||||
c.certsStorage.Set(hostname, tlsCertificate)
|
||||
return tlsCertificate, nil
|
||||
}
|
||||
|
||||
// GenerateAndSave generate CA private key and CA certificate and dump them to file
|
||||
func GenerateAndSave(caPath string, caKeyPath string) error {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(time.Now().Unix()),
|
||||
Subject: pkix.Name{
|
||||
Country: []string{"US"},
|
||||
CommonName: "Clash Root CA",
|
||||
Organization: []string{"Clash Trust Services"},
|
||||
},
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
NotBefore: time.Now().Add(-(time.Hour * 24 * 60)),
|
||||
NotAfter: time.Now().Add(time.Hour * 24 * 365 * 25),
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
}
|
||||
|
||||
caRaw, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, privateKey.Public(), privateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
caOut, err := os.OpenFile(caPath, os.O_CREATE|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(caOut *os.File) {
|
||||
_ = caOut.Close()
|
||||
}(caOut)
|
||||
|
||||
if err = pem.Encode(caOut, &pem.Block{Type: "CERTIFICATE", Bytes: caRaw}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
caKeyOut, err := os.OpenFile(caKeyPath, os.O_CREATE|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(caKeyOut *os.File) {
|
||||
_ = caKeyOut.Close()
|
||||
}(caKeyOut)
|
||||
|
||||
if err = pem.Encode(caKeyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
76
common/cert/cert_test.go
Normal file
76
common/cert/cert_test.go
Normal file
@ -0,0 +1,76 @@
|
||||
package cert
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCert(t *testing.T) {
|
||||
ca, privateKey, err := NewAuthority("Clash ca", "Clash", 24*time.Hour)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, ca)
|
||||
assert.NotNil(t, privateKey)
|
||||
|
||||
c, err := NewConfig(ca, privateKey, nil)
|
||||
assert.Nil(t, err)
|
||||
|
||||
c.SetValidity(20 * time.Hour)
|
||||
c.SetOrganization("Test Organization")
|
||||
|
||||
conf := c.NewTLSConfigForHost("example.org")
|
||||
assert.Equal(t, []string{"http/1.1"}, conf.NextProtos)
|
||||
assert.True(t, conf.InsecureSkipVerify)
|
||||
|
||||
// Test generating a certificate
|
||||
clientHello := &tls.ClientHelloInfo{
|
||||
ServerName: "example.org",
|
||||
}
|
||||
tlsCert, err := conf.GetCertificate(clientHello)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, tlsCert)
|
||||
|
||||
// Assert certificate details
|
||||
x509c := tlsCert.Leaf
|
||||
assert.Equal(t, "example.org", x509c.Subject.CommonName)
|
||||
assert.Nil(t, x509c.VerifyHostname("example.org"))
|
||||
assert.Equal(t, []string{"Test Organization"}, x509c.Subject.Organization)
|
||||
assert.NotNil(t, x509c.SubjectKeyId)
|
||||
assert.True(t, x509c.BasicConstraintsValid)
|
||||
assert.True(t, x509c.KeyUsage&x509.KeyUsageKeyEncipherment == x509.KeyUsageKeyEncipherment)
|
||||
assert.True(t, x509c.KeyUsage&x509.KeyUsageDigitalSignature == x509.KeyUsageDigitalSignature)
|
||||
assert.Equal(t, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, x509c.ExtKeyUsage)
|
||||
assert.Equal(t, []string{"example.org"}, x509c.DNSNames)
|
||||
assert.True(t, x509c.NotBefore.Before(time.Now().Add(-2*time.Hour)))
|
||||
assert.True(t, x509c.NotAfter.After(time.Now().Add(2*time.Hour)))
|
||||
|
||||
// Check that certificate is cached
|
||||
tlsCert2, err := c.GetOrCreateCert("example.org")
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, tlsCert == tlsCert2)
|
||||
|
||||
// Check the certificate for an IP
|
||||
tlsCertForIP, err := c.GetOrCreateCert("192.168.0.1:443")
|
||||
assert.Nil(t, err)
|
||||
x509c = tlsCertForIP.Leaf
|
||||
assert.Equal(t, 1, len(x509c.IPAddresses))
|
||||
assert.True(t, net.ParseIP("192.168.0.1").Equal(x509c.IPAddresses[0]))
|
||||
}
|
||||
|
||||
func TestGenerateAndSave(t *testing.T) {
|
||||
caPath := "ca.crt"
|
||||
caKeyPath := "ca.key"
|
||||
|
||||
err := GenerateAndSave(caPath, caKeyPath)
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
_ = os.Remove(caPath)
|
||||
_ = os.Remove(caKeyPath)
|
||||
}
|
32
common/cert/storage.go
Normal file
32
common/cert/storage.go
Normal file
@ -0,0 +1,32 @@
|
||||
package cert
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/common/cache"
|
||||
)
|
||||
|
||||
var TTL = time.Hour * 2
|
||||
|
||||
// AutoGCCertsStorage cache with the generated certificates, auto released after TTL
|
||||
type AutoGCCertsStorage struct {
|
||||
certsCache *cache.Cache[string, *tls.Certificate]
|
||||
}
|
||||
|
||||
// Get gets the certificate from the storage
|
||||
func (c *AutoGCCertsStorage) Get(key string) (*tls.Certificate, bool) {
|
||||
ca := c.certsCache.Get(key)
|
||||
return ca, ca != nil
|
||||
}
|
||||
|
||||
// Set saves the certificate to the storage
|
||||
func (c *AutoGCCertsStorage) Set(key string, cert *tls.Certificate) {
|
||||
c.certsCache.Put(key, cert, TTL)
|
||||
}
|
||||
|
||||
func NewAutoGCCertsStorage() *AutoGCCertsStorage {
|
||||
return &AutoGCCertsStorage{
|
||||
certsCache: cache.New[string, *tls.Certificate](TTL),
|
||||
}
|
||||
}
|
@ -5,13 +5,13 @@ import (
|
||||
)
|
||||
|
||||
// Queue is a simple concurrent safe queue
|
||||
type Queue struct {
|
||||
items []any
|
||||
type Queue[T any] struct {
|
||||
items []T
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// Put add the item to the queue.
|
||||
func (q *Queue) Put(items ...any) {
|
||||
func (q *Queue[T]) Put(items ...T) {
|
||||
if len(items) == 0 {
|
||||
return
|
||||
}
|
||||
@ -22,9 +22,9 @@ func (q *Queue) Put(items ...any) {
|
||||
}
|
||||
|
||||
// Pop returns the head of items.
|
||||
func (q *Queue) Pop() any {
|
||||
func (q *Queue[T]) Pop() T {
|
||||
if len(q.items) == 0 {
|
||||
return nil
|
||||
return GetZero[T]()
|
||||
}
|
||||
|
||||
q.lock.Lock()
|
||||
@ -35,9 +35,9 @@ func (q *Queue) Pop() any {
|
||||
}
|
||||
|
||||
// Last returns the last of item.
|
||||
func (q *Queue) Last() any {
|
||||
func (q *Queue[T]) Last() T {
|
||||
if len(q.items) == 0 {
|
||||
return nil
|
||||
return GetZero[T]()
|
||||
}
|
||||
|
||||
q.lock.RLock()
|
||||
@ -47,8 +47,8 @@ func (q *Queue) Last() any {
|
||||
}
|
||||
|
||||
// Copy get the copy of queue.
|
||||
func (q *Queue) Copy() []any {
|
||||
items := []any{}
|
||||
func (q *Queue[T]) Copy() []T {
|
||||
items := []T{}
|
||||
q.lock.RLock()
|
||||
items = append(items, q.items...)
|
||||
q.lock.RUnlock()
|
||||
@ -56,7 +56,7 @@ func (q *Queue) Copy() []any {
|
||||
}
|
||||
|
||||
// Len returns the number of items in this queue.
|
||||
func (q *Queue) Len() int64 {
|
||||
func (q *Queue[T]) Len() int64 {
|
||||
q.lock.Lock()
|
||||
defer q.lock.Unlock()
|
||||
|
||||
@ -64,8 +64,13 @@ func (q *Queue) Len() int64 {
|
||||
}
|
||||
|
||||
// New is a constructor for a new concurrent safe queue.
|
||||
func New(hint int64) *Queue {
|
||||
return &Queue{
|
||||
items: make([]any, 0, hint),
|
||||
func New[T any](hint int64) *Queue[T] {
|
||||
return &Queue[T]{
|
||||
items: make([]T, 0, hint),
|
||||
}
|
||||
}
|
||||
|
||||
func GetZero[T any]() T {
|
||||
var result T
|
||||
return result
|
||||
}
|
||||
|
148
common/snifer/tls/sniff.go
Normal file
148
common/snifer/tls/sniff.go
Normal file
@ -0,0 +1,148 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var ErrNoClue = errors.New("not enough information for making a decision")
|
||||
|
||||
type SniffHeader struct {
|
||||
domain string
|
||||
}
|
||||
|
||||
func (h *SniffHeader) Protocol() string {
|
||||
return "tls"
|
||||
}
|
||||
|
||||
func (h *SniffHeader) Domain() string {
|
||||
return h.domain
|
||||
}
|
||||
|
||||
var (
|
||||
errNotTLS = errors.New("not TLS header")
|
||||
errNotClientHello = errors.New("not client hello")
|
||||
)
|
||||
|
||||
func IsValidTLSVersion(major, minor byte) bool {
|
||||
return major == 3
|
||||
}
|
||||
|
||||
// ReadClientHello returns server name (if any) from TLS client hello message.
|
||||
// https://github.com/golang/go/blob/master/src/crypto/tls/handshake_messages.go#L300
|
||||
func ReadClientHello(data []byte, h *SniffHeader) error {
|
||||
if len(data) < 42 {
|
||||
return ErrNoClue
|
||||
}
|
||||
sessionIDLen := int(data[38])
|
||||
if sessionIDLen > 32 || len(data) < 39+sessionIDLen {
|
||||
return ErrNoClue
|
||||
}
|
||||
data = data[39+sessionIDLen:]
|
||||
if len(data) < 2 {
|
||||
return ErrNoClue
|
||||
}
|
||||
// cipherSuiteLen is the number of bytes of cipher suite numbers. Since
|
||||
// they are uint16s, the number must be even.
|
||||
cipherSuiteLen := int(data[0])<<8 | int(data[1])
|
||||
if cipherSuiteLen%2 == 1 || len(data) < 2+cipherSuiteLen {
|
||||
return errNotClientHello
|
||||
}
|
||||
data = data[2+cipherSuiteLen:]
|
||||
if len(data) < 1 {
|
||||
return ErrNoClue
|
||||
}
|
||||
compressionMethodsLen := int(data[0])
|
||||
if len(data) < 1+compressionMethodsLen {
|
||||
return ErrNoClue
|
||||
}
|
||||
data = data[1+compressionMethodsLen:]
|
||||
|
||||
if len(data) == 0 {
|
||||
return errNotClientHello
|
||||
}
|
||||
if len(data) < 2 {
|
||||
return errNotClientHello
|
||||
}
|
||||
|
||||
extensionsLength := int(data[0])<<8 | int(data[1])
|
||||
data = data[2:]
|
||||
if extensionsLength != len(data) {
|
||||
return errNotClientHello
|
||||
}
|
||||
|
||||
for len(data) != 0 {
|
||||
if len(data) < 4 {
|
||||
return errNotClientHello
|
||||
}
|
||||
extension := uint16(data[0])<<8 | uint16(data[1])
|
||||
length := int(data[2])<<8 | int(data[3])
|
||||
data = data[4:]
|
||||
if len(data) < length {
|
||||
return errNotClientHello
|
||||
}
|
||||
|
||||
if extension == 0x00 { /* extensionServerName */
|
||||
d := data[:length]
|
||||
if len(d) < 2 {
|
||||
return errNotClientHello
|
||||
}
|
||||
namesLen := int(d[0])<<8 | int(d[1])
|
||||
d = d[2:]
|
||||
if len(d) != namesLen {
|
||||
return errNotClientHello
|
||||
}
|
||||
for len(d) > 0 {
|
||||
if len(d) < 3 {
|
||||
return errNotClientHello
|
||||
}
|
||||
nameType := d[0]
|
||||
nameLen := int(d[1])<<8 | int(d[2])
|
||||
d = d[3:]
|
||||
if len(d) < nameLen {
|
||||
return errNotClientHello
|
||||
}
|
||||
if nameType == 0 {
|
||||
serverName := string(d[:nameLen])
|
||||
// An SNI value may not include a
|
||||
// trailing dot. See
|
||||
// https://tools.ietf.org/html/rfc6066#section-3.
|
||||
if strings.HasSuffix(serverName, ".") {
|
||||
return errNotClientHello
|
||||
}
|
||||
h.domain = serverName
|
||||
return nil
|
||||
}
|
||||
d = d[nameLen:]
|
||||
}
|
||||
}
|
||||
data = data[length:]
|
||||
}
|
||||
|
||||
return errNotTLS
|
||||
}
|
||||
|
||||
func SniffTLS(b []byte) (*SniffHeader, error) {
|
||||
if len(b) < 5 {
|
||||
return nil, ErrNoClue
|
||||
}
|
||||
|
||||
if b[0] != 0x16 /* TLS Handshake */ {
|
||||
return nil, errNotTLS
|
||||
}
|
||||
if !IsValidTLSVersion(b[1], b[2]) {
|
||||
return nil, errNotTLS
|
||||
}
|
||||
headerLen := int(binary.BigEndian.Uint16(b[3:5]))
|
||||
if 5+headerLen > len(b) {
|
||||
return nil, ErrNoClue
|
||||
}
|
||||
|
||||
h := &SniffHeader{}
|
||||
err := ReadClientHello(b[5:5+headerLen], h)
|
||||
if err == nil {
|
||||
return h, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
159
common/snifer/tls/sniff_test.go
Normal file
159
common/snifer/tls/sniff_test.go
Normal file
@ -0,0 +1,159 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTLSHeaders(t *testing.T) {
|
||||
cases := []struct {
|
||||
input []byte
|
||||
domain string
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
input: []byte{
|
||||
0x16, 0x03, 0x01, 0x00, 0xc8, 0x01, 0x00, 0x00,
|
||||
0xc4, 0x03, 0x03, 0x1a, 0xac, 0xb2, 0xa8, 0xfe,
|
||||
0xb4, 0x96, 0x04, 0x5b, 0xca, 0xf7, 0xc1, 0xf4,
|
||||
0x2e, 0x53, 0x24, 0x6e, 0x34, 0x0c, 0x58, 0x36,
|
||||
0x71, 0x97, 0x59, 0xe9, 0x41, 0x66, 0xe2, 0x43,
|
||||
0xa0, 0x13, 0xb6, 0x00, 0x00, 0x20, 0x1a, 0x1a,
|
||||
0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30,
|
||||
0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0x14, 0xcc, 0x13,
|
||||
0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d,
|
||||
0x00, 0x2f, 0x00, 0x35, 0x00, 0x0a, 0x01, 0x00,
|
||||
0x00, 0x7b, 0xba, 0xba, 0x00, 0x00, 0xff, 0x01,
|
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00,
|
||||
0x14, 0x00, 0x00, 0x11, 0x63, 0x2e, 0x73, 0x2d,
|
||||
0x6d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66,
|
||||
0x74, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x17, 0x00,
|
||||
0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x0d, 0x00,
|
||||
0x14, 0x00, 0x12, 0x04, 0x03, 0x08, 0x04, 0x04,
|
||||
0x01, 0x05, 0x03, 0x08, 0x05, 0x05, 0x01, 0x08,
|
||||
0x06, 0x06, 0x01, 0x02, 0x01, 0x00, 0x05, 0x00,
|
||||
0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12,
|
||||
0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c,
|
||||
0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70,
|
||||
0x2f, 0x31, 0x2e, 0x31, 0x00, 0x0b, 0x00, 0x02,
|
||||
0x01, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08,
|
||||
0xaa, 0xaa, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18,
|
||||
0xaa, 0xaa, 0x00, 0x01, 0x00,
|
||||
},
|
||||
domain: "c.s-microsoft.com",
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
input: []byte{
|
||||
0x16, 0x03, 0x01, 0x00, 0xee, 0x01, 0x00, 0x00,
|
||||
0xea, 0x03, 0x03, 0xe7, 0x91, 0x9e, 0x93, 0xca,
|
||||
0x78, 0x1b, 0x3c, 0xe0, 0x65, 0x25, 0x58, 0xb5,
|
||||
0x93, 0xe1, 0x0f, 0x85, 0xec, 0x9a, 0x66, 0x8e,
|
||||
0x61, 0x82, 0x88, 0xc8, 0xfc, 0xae, 0x1e, 0xca,
|
||||
0xd7, 0xa5, 0x63, 0x20, 0xbd, 0x1c, 0x00, 0x00,
|
||||
0x8b, 0xee, 0x09, 0xe3, 0x47, 0x6a, 0x0e, 0x74,
|
||||
0xb0, 0xbc, 0xa3, 0x02, 0xa7, 0x35, 0xe8, 0x85,
|
||||
0x70, 0x7c, 0x7a, 0xf0, 0x00, 0xdf, 0x4a, 0xea,
|
||||
0x87, 0x01, 0x14, 0x91, 0x00, 0x20, 0xea, 0xea,
|
||||
0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30,
|
||||
0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0x14, 0xcc, 0x13,
|
||||
0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d,
|
||||
0x00, 0x2f, 0x00, 0x35, 0x00, 0x0a, 0x01, 0x00,
|
||||
0x00, 0x81, 0x9a, 0x9a, 0x00, 0x00, 0xff, 0x01,
|
||||
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00,
|
||||
0x16, 0x00, 0x00, 0x13, 0x77, 0x77, 0x77, 0x30,
|
||||
0x37, 0x2e, 0x63, 0x6c, 0x69, 0x63, 0x6b, 0x74,
|
||||
0x61, 0x6c, 0x65, 0x2e, 0x6e, 0x65, 0x74, 0x00,
|
||||
0x17, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00,
|
||||
0x0d, 0x00, 0x14, 0x00, 0x12, 0x04, 0x03, 0x08,
|
||||
0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05,
|
||||
0x01, 0x08, 0x06, 0x06, 0x01, 0x02, 0x01, 0x00,
|
||||
0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x12, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e,
|
||||
0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74,
|
||||
0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x75, 0x50,
|
||||
0x00, 0x00, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00,
|
||||
0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x9a, 0x9a,
|
||||
0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x8a, 0x8a,
|
||||
0x00, 0x01, 0x00,
|
||||
},
|
||||
domain: "www07.clicktale.net",
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
input: []byte{
|
||||
0x16, 0x03, 0x01, 0x00, 0xe6, 0x01, 0x00, 0x00, 0xe2, 0x03, 0x03, 0x81, 0x47, 0xc1,
|
||||
0x66, 0xd5, 0x1b, 0xfa, 0x4b, 0xb5, 0xe0, 0x2a, 0xe1, 0xa7, 0x87, 0x13, 0x1d, 0x11, 0xaa, 0xc6,
|
||||
0xce, 0xfc, 0x7f, 0xab, 0x94, 0xc8, 0x62, 0xad, 0xc8, 0xab, 0x0c, 0xdd, 0xcb, 0x20, 0x6f, 0x9d,
|
||||
0x07, 0xf1, 0x95, 0x3e, 0x99, 0xd8, 0xf3, 0x6d, 0x97, 0xee, 0x19, 0x0b, 0x06, 0x1b, 0xf4, 0x84,
|
||||
0x0b, 0xb6, 0x8f, 0xcc, 0xde, 0xe2, 0xd0, 0x2d, 0x6b, 0x0c, 0x1f, 0x52, 0x53, 0x13, 0x00, 0x08,
|
||||
0x13, 0x02, 0x13, 0x03, 0x13, 0x01, 0x00, 0xff, 0x01, 0x00, 0x00, 0x91, 0x00, 0x00, 0x00, 0x0c,
|
||||
0x00, 0x0a, 0x00, 0x00, 0x07, 0x64, 0x6f, 0x67, 0x66, 0x69, 0x73, 0x68, 0x00, 0x0b, 0x00, 0x04,
|
||||
0x03, 0x00, 0x01, 0x02, 0x00, 0x0a, 0x00, 0x0c, 0x00, 0x0a, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x1e,
|
||||
0x00, 0x19, 0x00, 0x18, 0x00, 0x23, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00,
|
||||
0x00, 0x0d, 0x00, 0x1e, 0x00, 0x1c, 0x04, 0x03, 0x05, 0x03, 0x06, 0x03, 0x08, 0x07, 0x08, 0x08,
|
||||
0x08, 0x09, 0x08, 0x0a, 0x08, 0x0b, 0x08, 0x04, 0x08, 0x05, 0x08, 0x06, 0x04, 0x01, 0x05, 0x01,
|
||||
0x06, 0x01, 0x00, 0x2b, 0x00, 0x07, 0x06, 0x7f, 0x1c, 0x7f, 0x1b, 0x7f, 0x1a, 0x00, 0x2d, 0x00,
|
||||
0x02, 0x01, 0x01, 0x00, 0x33, 0x00, 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20, 0x2f, 0x35, 0x0c,
|
||||
0xb6, 0x90, 0x0a, 0xb7, 0xd5, 0xc4, 0x1b, 0x2f, 0x60, 0xaa, 0x56, 0x7b, 0x3f, 0x71, 0xc8, 0x01,
|
||||
0x7e, 0x86, 0xd3, 0xb7, 0x0c, 0x29, 0x1a, 0x9e, 0x5b, 0x38, 0x3f, 0x01, 0x72,
|
||||
},
|
||||
domain: "dogfish",
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
input: []byte{
|
||||
0x16, 0x03, 0x01, 0x01, 0x03, 0x01, 0x00, 0x00,
|
||||
0xff, 0x03, 0x03, 0x3d, 0x89, 0x52, 0x9e, 0xee,
|
||||
0xbe, 0x17, 0x63, 0x75, 0xef, 0x29, 0xbd, 0x14,
|
||||
0x6a, 0x49, 0xe0, 0x2c, 0x37, 0x57, 0x71, 0x62,
|
||||
0x82, 0x44, 0x94, 0x8f, 0x6e, 0x94, 0x08, 0x45,
|
||||
0x7f, 0xdb, 0xc1, 0x00, 0x00, 0x3e, 0xc0, 0x2c,
|
||||
0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8,
|
||||
0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f, 0x00, 0x9e,
|
||||
0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23,
|
||||
0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a, 0xc0, 0x14,
|
||||
0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33,
|
||||
0x00, 0x9d, 0x00, 0x9c, 0x13, 0x02, 0x13, 0x03,
|
||||
0x13, 0x01, 0x00, 0x3d, 0x00, 0x3c, 0x00, 0x35,
|
||||
0x00, 0x2f, 0x00, 0xff, 0x01, 0x00, 0x00, 0x98,
|
||||
0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x00,
|
||||
0x0b, 0x31, 0x30, 0x2e, 0x34, 0x32, 0x2e, 0x30,
|
||||
0x2e, 0x32, 0x34, 0x33, 0x00, 0x0b, 0x00, 0x04,
|
||||
0x03, 0x00, 0x01, 0x02, 0x00, 0x0a, 0x00, 0x0a,
|
||||
0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x19,
|
||||
0x00, 0x18, 0x00, 0x23, 0x00, 0x00, 0x00, 0x0d,
|
||||
0x00, 0x20, 0x00, 0x1e, 0x04, 0x03, 0x05, 0x03,
|
||||
0x06, 0x03, 0x08, 0x04, 0x08, 0x05, 0x08, 0x06,
|
||||
0x04, 0x01, 0x05, 0x01, 0x06, 0x01, 0x02, 0x03,
|
||||
0x02, 0x01, 0x02, 0x02, 0x04, 0x02, 0x05, 0x02,
|
||||
0x06, 0x02, 0x00, 0x16, 0x00, 0x00, 0x00, 0x17,
|
||||
0x00, 0x00, 0x00, 0x2b, 0x00, 0x09, 0x08, 0x7f,
|
||||
0x14, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, 0x00,
|
||||
0x2d, 0x00, 0x03, 0x02, 0x01, 0x00, 0x00, 0x28,
|
||||
0x00, 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20,
|
||||
0x13, 0x7c, 0x6e, 0x97, 0xc4, 0xfd, 0x09, 0x2e,
|
||||
0x70, 0x2f, 0x73, 0x5a, 0x9b, 0x57, 0x4d, 0x5f,
|
||||
0x2b, 0x73, 0x2c, 0xa5, 0x4a, 0x98, 0x40, 0x3d,
|
||||
0x75, 0x6e, 0xb4, 0x76, 0xf9, 0x48, 0x8f, 0x36,
|
||||
},
|
||||
domain: "10.42.0.243",
|
||||
err: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range cases {
|
||||
header, err := SniffTLS(test.input)
|
||||
if test.err {
|
||||
if err == nil {
|
||||
t.Errorf("Exepct error but nil in test %v", test)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Expect no error but actually %s in test %v", err.Error(), test)
|
||||
}
|
||||
if header.Domain() != test.domain {
|
||||
t.Error("expect domain ", test.domain, " but got ", header.Domain())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -7,16 +7,15 @@ import (
|
||||
)
|
||||
|
||||
type memoryStore struct {
|
||||
cache *cache.LruCache
|
||||
cacheIP *cache.LruCache[string, net.IP]
|
||||
cacheHost *cache.LruCache[uint32, string]
|
||||
}
|
||||
|
||||
// GetByHost implements store.GetByHost
|
||||
func (m *memoryStore) GetByHost(host string) (net.IP, bool) {
|
||||
if elm, exist := m.cache.Get(host); exist {
|
||||
ip := elm.(net.IP)
|
||||
|
||||
if ip, exist := m.cacheIP.Get(host); exist {
|
||||
// ensure ip --> host on head of linked list
|
||||
m.cache.Get(ipToUint(ip.To4()))
|
||||
m.cacheHost.Get(ipToUint(ip.To4()))
|
||||
return ip, true
|
||||
}
|
||||
|
||||
@ -25,16 +24,14 @@ func (m *memoryStore) GetByHost(host string) (net.IP, bool) {
|
||||
|
||||
// PutByHost implements store.PutByHost
|
||||
func (m *memoryStore) PutByHost(host string, ip net.IP) {
|
||||
m.cache.Set(host, ip)
|
||||
m.cacheIP.Set(host, ip)
|
||||
}
|
||||
|
||||
// GetByIP implements store.GetByIP
|
||||
func (m *memoryStore) GetByIP(ip net.IP) (string, bool) {
|
||||
if elm, exist := m.cache.Get(ipToUint(ip.To4())); exist {
|
||||
host := elm.(string)
|
||||
|
||||
if host, exist := m.cacheHost.Get(ipToUint(ip.To4())); exist {
|
||||
// ensure host --> ip on head of linked list
|
||||
m.cache.Get(host)
|
||||
m.cacheIP.Get(host)
|
||||
return host, true
|
||||
}
|
||||
|
||||
@ -43,32 +40,41 @@ func (m *memoryStore) GetByIP(ip net.IP) (string, bool) {
|
||||
|
||||
// PutByIP implements store.PutByIP
|
||||
func (m *memoryStore) PutByIP(ip net.IP, host string) {
|
||||
m.cache.Set(ipToUint(ip.To4()), host)
|
||||
m.cacheHost.Set(ipToUint(ip.To4()), host)
|
||||
}
|
||||
|
||||
// DelByIP implements store.DelByIP
|
||||
func (m *memoryStore) DelByIP(ip net.IP) {
|
||||
ipNum := ipToUint(ip.To4())
|
||||
if elm, exist := m.cache.Get(ipNum); exist {
|
||||
m.cache.Delete(elm.(string))
|
||||
if host, exist := m.cacheHost.Get(ipNum); exist {
|
||||
m.cacheIP.Delete(host)
|
||||
}
|
||||
m.cache.Delete(ipNum)
|
||||
m.cacheHost.Delete(ipNum)
|
||||
}
|
||||
|
||||
// Exist implements store.Exist
|
||||
func (m *memoryStore) Exist(ip net.IP) bool {
|
||||
return m.cache.Exist(ipToUint(ip.To4()))
|
||||
return m.cacheHost.Exist(ipToUint(ip.To4()))
|
||||
}
|
||||
|
||||
// CloneTo implements store.CloneTo
|
||||
// only for memoryStore to memoryStore
|
||||
func (m *memoryStore) CloneTo(store store) {
|
||||
if ms, ok := store.(*memoryStore); ok {
|
||||
m.cache.CloneTo(ms.cache)
|
||||
m.cacheIP.CloneTo(ms.cacheIP)
|
||||
m.cacheHost.CloneTo(ms.cacheHost)
|
||||
}
|
||||
}
|
||||
|
||||
// FlushFakeIP implements store.FlushFakeIP
|
||||
func (m *memoryStore) FlushFakeIP() error {
|
||||
return m.cache.Clear()
|
||||
_ = m.cacheIP.Clear()
|
||||
return m.cacheHost.Clear()
|
||||
}
|
||||
|
||||
func newMemoryStore(size int) *memoryStore {
|
||||
return &memoryStore{
|
||||
cacheIP: cache.NewLRUCache[string, net.IP](cache.WithSize[string, net.IP](size)),
|
||||
cacheHost: cache.NewLRUCache[uint32, string](cache.WithSize[uint32, string](size)),
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/Dreamacro/clash/common/cache"
|
||||
"github.com/Dreamacro/clash/component/profile/cachefile"
|
||||
"github.com/Dreamacro/clash/component/trie"
|
||||
)
|
||||
@ -29,7 +28,7 @@ type Pool struct {
|
||||
broadcast uint32
|
||||
offset uint32
|
||||
mux sync.Mutex
|
||||
host *trie.DomainTrie
|
||||
host *trie.DomainTrie[bool]
|
||||
ipnet *net.IPNet
|
||||
store store
|
||||
}
|
||||
@ -139,7 +138,7 @@ func uintToIP(v uint32) net.IP {
|
||||
|
||||
type Options struct {
|
||||
IPNet *net.IPNet
|
||||
Host *trie.DomainTrie
|
||||
Host *trie.DomainTrie[bool]
|
||||
|
||||
// Size sets the maximum number of entries in memory
|
||||
// and does not work if Persistence is true
|
||||
@ -175,9 +174,7 @@ func New(options Options) (*Pool, error) {
|
||||
cache: cachefile.Cache(),
|
||||
}
|
||||
} else {
|
||||
pool.store = &memoryStore{
|
||||
cache: cache.NewLRUCache(cache.WithSize(options.Size * 2)),
|
||||
}
|
||||
pool.store = newMemoryStore(options.Size)
|
||||
}
|
||||
|
||||
return pool, nil
|
||||
|
@ -100,8 +100,8 @@ func TestPool_CycleUsed(t *testing.T) {
|
||||
|
||||
func TestPool_Skip(t *testing.T) {
|
||||
_, ipnet, _ := net.ParseCIDR("192.168.0.1/29")
|
||||
tree := trie.New()
|
||||
tree.Insert("example.com", tree)
|
||||
tree := trie.New[bool]()
|
||||
tree.Insert("example.com", true)
|
||||
pools, tempfile, err := createPools(Options{
|
||||
IPNet: ipnet,
|
||||
Size: 10,
|
||||
|
@ -33,7 +33,7 @@ func (g GeoIPCache) Set(key string, value *router.GeoIP) {
|
||||
}
|
||||
|
||||
func (g GeoIPCache) Unmarshal(filename, code string) (*router.GeoIP, error) {
|
||||
asset := C.Path.GetAssetLocation(filename)
|
||||
asset := C.Path.Resolve(filename)
|
||||
idx := strings.ToLower(asset + ":" + code)
|
||||
if g.Has(idx) {
|
||||
return g.Get(idx), nil
|
||||
@ -98,7 +98,7 @@ func (g GeoSiteCache) Set(key string, value *router.GeoSite) {
|
||||
}
|
||||
|
||||
func (g GeoSiteCache) Unmarshal(filename, code string) (*router.GeoSite, error) {
|
||||
asset := C.Path.GetAssetLocation(filename)
|
||||
asset := C.Path.Resolve(filename)
|
||||
idx := strings.ToLower(asset + ":" + code)
|
||||
if g.Has(idx) {
|
||||
return g.Get(idx), nil
|
||||
|
@ -26,7 +26,7 @@ func ReadFile(path string) ([]byte, error) {
|
||||
}
|
||||
|
||||
func ReadAsset(file string) ([]byte, error) {
|
||||
return ReadFile(C.Path.GetAssetLocation(file))
|
||||
return ReadFile(C.Path.Resolve(file))
|
||||
}
|
||||
|
||||
func loadIP(filename, country string) ([]*router.CIDR, error) {
|
||||
|
@ -14,6 +14,7 @@ type Enhancer interface {
|
||||
IsExistFakeIP(net.IP) bool
|
||||
FindHostByIP(net.IP) (string, bool)
|
||||
FlushFakeIP() error
|
||||
InsertHostByIP(net.IP, string)
|
||||
}
|
||||
|
||||
func FakeIPEnabled() bool {
|
||||
@ -56,6 +57,12 @@ func IsExistFakeIP(ip net.IP) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func InsertHostByIP(ip net.IP, host string) {
|
||||
if mapper := DefaultHostMapper; mapper != nil {
|
||||
mapper.InsertHostByIP(ip, host)
|
||||
}
|
||||
}
|
||||
|
||||
func FindHostByIP(ip net.IP) (string, bool) {
|
||||
if mapper := DefaultHostMapper; mapper != nil {
|
||||
return mapper.FindHostByIP(ip)
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -23,7 +24,7 @@ var (
|
||||
DisableIPv6 = true
|
||||
|
||||
// DefaultHosts aim to resolve hosts
|
||||
DefaultHosts = trie.New()
|
||||
DefaultHosts = trie.New[netip.Addr]()
|
||||
|
||||
// DefaultDNSTimeout defined the default dns request timeout
|
||||
DefaultDNSTimeout = time.Second * 5
|
||||
@ -48,8 +49,8 @@ func ResolveIPv4(host string) (net.IP, error) {
|
||||
|
||||
func ResolveIPv4WithResolver(host string, r Resolver) (net.IP, error) {
|
||||
if node := DefaultHosts.Search(host); node != nil {
|
||||
if ip := node.Data.(net.IP).To4(); ip != nil {
|
||||
return ip, nil
|
||||
if ip := node.Data; ip.Is4() {
|
||||
return ip.AsSlice(), nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,8 +93,8 @@ func ResolveIPv6WithResolver(host string, r Resolver) (net.IP, error) {
|
||||
}
|
||||
|
||||
if node := DefaultHosts.Search(host); node != nil {
|
||||
if ip := node.Data.(net.IP).To16(); ip != nil {
|
||||
return ip, nil
|
||||
if ip := node.Data; ip.Is6() {
|
||||
return ip.AsSlice(), nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,7 +129,8 @@ func ResolveIPv6WithResolver(host string, r Resolver) (net.IP, error) {
|
||||
// ResolveIPWithResolver same as ResolveIP, but with a resolver
|
||||
func ResolveIPWithResolver(host string, r Resolver) (net.IP, error) {
|
||||
if node := DefaultHosts.Search(host); node != nil {
|
||||
return node.Data.(net.IP), nil
|
||||
ip := node.Data
|
||||
return ip.Unmap().AsSlice(), nil
|
||||
}
|
||||
|
||||
if r != nil {
|
||||
|
@ -17,8 +17,8 @@ var ErrInvalidDomain = errors.New("invalid domain")
|
||||
|
||||
// DomainTrie contains the main logic for adding and searching nodes for domain segments.
|
||||
// support wildcard domain (e.g *.google.com)
|
||||
type DomainTrie struct {
|
||||
root *Node
|
||||
type DomainTrie[T comparable] struct {
|
||||
root *Node[T]
|
||||
}
|
||||
|
||||
func ValidAndSplitDomain(domain string) ([]string, bool) {
|
||||
@ -51,7 +51,7 @@ func ValidAndSplitDomain(domain string) ([]string, bool) {
|
||||
// 3. subdomain.*.example.com
|
||||
// 4. .example.com
|
||||
// 5. +.example.com
|
||||
func (t *DomainTrie) Insert(domain string, data any) error {
|
||||
func (t *DomainTrie[T]) Insert(domain string, data T) error {
|
||||
parts, valid := ValidAndSplitDomain(domain)
|
||||
if !valid {
|
||||
return ErrInvalidDomain
|
||||
@ -68,13 +68,13 @@ func (t *DomainTrie) Insert(domain string, data any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *DomainTrie) insert(parts []string, data any) {
|
||||
func (t *DomainTrie[T]) insert(parts []string, data T) {
|
||||
node := t.root
|
||||
// reverse storage domain part to save space
|
||||
for i := len(parts) - 1; i >= 0; i-- {
|
||||
part := parts[i]
|
||||
if !node.hasChild(part) {
|
||||
node.addChild(part, newNode(nil))
|
||||
node.addChild(part, newNode(getZero[T]()))
|
||||
}
|
||||
|
||||
node = node.getChild(part)
|
||||
@ -88,7 +88,7 @@ func (t *DomainTrie) insert(parts []string, data any) {
|
||||
// 1. static part
|
||||
// 2. wildcard domain
|
||||
// 2. dot wildcard domain
|
||||
func (t *DomainTrie) Search(domain string) *Node {
|
||||
func (t *DomainTrie[T]) Search(domain string) *Node[T] {
|
||||
parts, valid := ValidAndSplitDomain(domain)
|
||||
if !valid || parts[0] == "" {
|
||||
return nil
|
||||
@ -96,26 +96,26 @@ func (t *DomainTrie) Search(domain string) *Node {
|
||||
|
||||
n := t.search(t.root, parts)
|
||||
|
||||
if n == nil || n.Data == nil {
|
||||
if n == nil || n.Data == getZero[T]() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func (t *DomainTrie) search(node *Node, parts []string) *Node {
|
||||
func (t *DomainTrie[T]) search(node *Node[T], parts []string) *Node[T] {
|
||||
if len(parts) == 0 {
|
||||
return node
|
||||
}
|
||||
|
||||
if c := node.getChild(parts[len(parts)-1]); c != nil {
|
||||
if n := t.search(c, parts[:len(parts)-1]); n != nil && n.Data != nil {
|
||||
if n := t.search(c, parts[:len(parts)-1]); n != nil && n.Data != getZero[T]() {
|
||||
return n
|
||||
}
|
||||
}
|
||||
|
||||
if c := node.getChild(wildcard); c != nil {
|
||||
if n := t.search(c, parts[:len(parts)-1]); n != nil && n.Data != nil {
|
||||
if n := t.search(c, parts[:len(parts)-1]); n != nil && n.Data != getZero[T]() {
|
||||
return n
|
||||
}
|
||||
}
|
||||
@ -124,6 +124,6 @@ func (t *DomainTrie) search(node *Node, parts []string) *Node {
|
||||
}
|
||||
|
||||
// New returns a new, empty Trie.
|
||||
func New() *DomainTrie {
|
||||
return &DomainTrie{root: newNode(nil)}
|
||||
func New[T comparable]() *DomainTrie[T] {
|
||||
return &DomainTrie[T]{root: newNode[T](getZero[T]())}
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
package trie
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var localIP = net.IP{127, 0, 0, 1}
|
||||
var localIP = netip.AddrFrom4([4]byte{127, 0, 0, 1})
|
||||
|
||||
func TestTrie_Basic(t *testing.T) {
|
||||
tree := New()
|
||||
tree := New[netip.Addr]()
|
||||
domains := []string{
|
||||
"example.com",
|
||||
"google.com",
|
||||
@ -23,7 +23,7 @@ func TestTrie_Basic(t *testing.T) {
|
||||
|
||||
node := tree.Search("example.com")
|
||||
assert.NotNil(t, node)
|
||||
assert.True(t, node.Data.(net.IP).Equal(localIP))
|
||||
assert.True(t, node.Data == localIP)
|
||||
assert.NotNil(t, tree.Insert("", localIP))
|
||||
assert.Nil(t, tree.Search(""))
|
||||
assert.NotNil(t, tree.Search("localhost"))
|
||||
@ -31,7 +31,7 @@ func TestTrie_Basic(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTrie_Wildcard(t *testing.T) {
|
||||
tree := New()
|
||||
tree := New[netip.Addr]()
|
||||
domains := []string{
|
||||
"*.example.com",
|
||||
"sub.*.example.com",
|
||||
@ -64,7 +64,7 @@ func TestTrie_Wildcard(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTrie_Priority(t *testing.T) {
|
||||
tree := New()
|
||||
tree := New[int]()
|
||||
domains := []string{
|
||||
".dev",
|
||||
"example.dev",
|
||||
@ -79,18 +79,18 @@ func TestTrie_Priority(t *testing.T) {
|
||||
}
|
||||
|
||||
for idx, domain := range domains {
|
||||
tree.Insert(domain, idx)
|
||||
tree.Insert(domain, idx+1)
|
||||
}
|
||||
|
||||
assertFn("test.dev", 0)
|
||||
assertFn("foo.bar.dev", 0)
|
||||
assertFn("example.dev", 1)
|
||||
assertFn("foo.example.dev", 2)
|
||||
assertFn("test.example.dev", 3)
|
||||
assertFn("test.dev", 1)
|
||||
assertFn("foo.bar.dev", 1)
|
||||
assertFn("example.dev", 2)
|
||||
assertFn("foo.example.dev", 3)
|
||||
assertFn("test.example.dev", 4)
|
||||
}
|
||||
|
||||
func TestTrie_Boundary(t *testing.T) {
|
||||
tree := New()
|
||||
tree := New[netip.Addr]()
|
||||
tree.Insert("*.dev", localIP)
|
||||
|
||||
assert.NotNil(t, tree.Insert(".", localIP))
|
||||
@ -99,7 +99,7 @@ func TestTrie_Boundary(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTrie_WildcardBoundary(t *testing.T) {
|
||||
tree := New()
|
||||
tree := New[netip.Addr]()
|
||||
tree.Insert("+.*", localIP)
|
||||
tree.Insert("stun.*.*.*", localIP)
|
||||
|
||||
|
@ -1,26 +1,31 @@
|
||||
package trie
|
||||
|
||||
// Node is the trie's node
|
||||
type Node struct {
|
||||
children map[string]*Node
|
||||
Data any
|
||||
type Node[T comparable] struct {
|
||||
children map[string]*Node[T]
|
||||
Data T
|
||||
}
|
||||
|
||||
func (n *Node) getChild(s string) *Node {
|
||||
func (n *Node[T]) getChild(s string) *Node[T] {
|
||||
return n.children[s]
|
||||
}
|
||||
|
||||
func (n *Node) hasChild(s string) bool {
|
||||
func (n *Node[T]) hasChild(s string) bool {
|
||||
return n.getChild(s) != nil
|
||||
}
|
||||
|
||||
func (n *Node) addChild(s string, child *Node) {
|
||||
func (n *Node[T]) addChild(s string, child *Node[T]) {
|
||||
n.children[s] = child
|
||||
}
|
||||
|
||||
func newNode(data any) *Node {
|
||||
return &Node{
|
||||
func newNode[T comparable](data T) *Node[T] {
|
||||
return &Node[T]{
|
||||
Data: data,
|
||||
children: map[string]*Node{},
|
||||
children: map[string]*Node[T]{},
|
||||
}
|
||||
}
|
||||
|
||||
func getZero[T comparable]() T {
|
||||
var result T
|
||||
return result
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import (
|
||||
"github.com/Dreamacro/clash/dns"
|
||||
"github.com/Dreamacro/clash/listener/tun/ipstack/commons"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
rewrites "github.com/Dreamacro/clash/rewrite"
|
||||
R "github.com/Dreamacro/clash/rule"
|
||||
T "github.com/Dreamacro/clash/tunnel"
|
||||
|
||||
@ -51,6 +52,7 @@ type Inbound struct {
|
||||
RedirPort int `json:"redir-port"`
|
||||
TProxyPort int `json:"tproxy-port"`
|
||||
MixedPort int `json:"mixed-port"`
|
||||
MitmPort int `json:"mitm-port"`
|
||||
Authentication []string `json:"authentication"`
|
||||
AllowLan bool `json:"allow-lan"`
|
||||
BindAddress string `json:"bind-address"`
|
||||
@ -74,7 +76,7 @@ type DNS struct {
|
||||
EnhancedMode C.DNSMode `yaml:"enhanced-mode"`
|
||||
DefaultNameserver []dns.NameServer `yaml:"default-nameserver"`
|
||||
FakeIPRange *fakeip.Pool
|
||||
Hosts *trie.DomainTrie
|
||||
Hosts *trie.DomainTrie[netip.Addr]
|
||||
NameServerPolicy map[string]dns.NameServer
|
||||
ProxyServerNameserver []dns.NameServer
|
||||
}
|
||||
@ -115,6 +117,12 @@ type IPTables struct {
|
||||
InboundInterface string `yaml:"inbound-interface" json:"inbound-interface"`
|
||||
}
|
||||
|
||||
// Mitm config
|
||||
type Mitm struct {
|
||||
Hosts *trie.DomainTrie[bool] `yaml:"hosts" json:"hosts"`
|
||||
Rules C.RewriteRule `yaml:"rules" json:"rules"`
|
||||
}
|
||||
|
||||
// Experimental config
|
||||
type Experimental struct{}
|
||||
|
||||
@ -123,9 +131,10 @@ type Config struct {
|
||||
General *General
|
||||
Tun *Tun
|
||||
IPTables *IPTables
|
||||
Mitm *Mitm
|
||||
DNS *DNS
|
||||
Experimental *Experimental
|
||||
Hosts *trie.DomainTrie
|
||||
Hosts *trie.DomainTrie[netip.Addr]
|
||||
Profile *Profile
|
||||
Rules []C.Rule
|
||||
RuleProviders map[string]C.Rule
|
||||
@ -166,12 +175,18 @@ type RawTun struct {
|
||||
AutoRoute bool `yaml:"auto-route" json:"auto-route"`
|
||||
}
|
||||
|
||||
type RawMitm struct {
|
||||
Hosts []string `yaml:"hosts" json:"hosts"`
|
||||
Rules []string `yaml:"rules" json:"rules"`
|
||||
}
|
||||
|
||||
type RawConfig struct {
|
||||
Port int `yaml:"port"`
|
||||
SocksPort int `yaml:"socks-port"`
|
||||
RedirPort int `yaml:"redir-port"`
|
||||
TProxyPort int `yaml:"tproxy-port"`
|
||||
MixedPort int `yaml:"mixed-port"`
|
||||
MitmPort int `yaml:"mitm-port"`
|
||||
Authentication []string `yaml:"authentication"`
|
||||
AllowLan bool `yaml:"allow-lan"`
|
||||
BindAddress string `yaml:"bind-address"`
|
||||
@ -189,6 +204,7 @@ type RawConfig struct {
|
||||
DNS RawDNS `yaml:"dns"`
|
||||
Tun RawTun `yaml:"tun"`
|
||||
IPTables IPTables `yaml:"iptables"`
|
||||
MITM RawMitm `yaml:"mitm"`
|
||||
Experimental Experimental `yaml:"experimental"`
|
||||
Profile Profile `yaml:"profile"`
|
||||
Proxy []map[string]any `yaml:"proxies"`
|
||||
@ -250,6 +266,10 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
|
||||
"tls://223.5.5.5:853",
|
||||
},
|
||||
},
|
||||
MITM: RawMitm{
|
||||
Hosts: []string{},
|
||||
Rules: []string{},
|
||||
},
|
||||
Profile: Profile{
|
||||
StoreSelected: true,
|
||||
},
|
||||
@ -314,6 +334,12 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
|
||||
}
|
||||
config.DNS = dnsCfg
|
||||
|
||||
mitm, err := parseMitm(rawCfg.MITM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Mitm = mitm
|
||||
|
||||
config.Users = parseAuthentication(rawCfg.Authentication)
|
||||
|
||||
return config, nil
|
||||
@ -338,6 +364,7 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
|
||||
RedirPort: cfg.RedirPort,
|
||||
TProxyPort: cfg.TProxyPort,
|
||||
MixedPort: cfg.MixedPort,
|
||||
MitmPort: cfg.MitmPort,
|
||||
AllowLan: cfg.AllowLan,
|
||||
BindAddress: cfg.BindAddress,
|
||||
},
|
||||
@ -546,24 +573,29 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[strin
|
||||
return rules, ruleProviders, nil
|
||||
}
|
||||
|
||||
func parseHosts(cfg *RawConfig) (*trie.DomainTrie, error) {
|
||||
tree := trie.New()
|
||||
func parseHosts(cfg *RawConfig) (*trie.DomainTrie[netip.Addr], error) {
|
||||
tree := trie.New[netip.Addr]()
|
||||
|
||||
// add default hosts
|
||||
if err := tree.Insert("localhost", net.IP{127, 0, 0, 1}); err != nil {
|
||||
if err := tree.Insert("localhost", netip.AddrFrom4([4]byte{127, 0, 0, 1})); err != nil {
|
||||
log.Errorln("insert localhost to host error: %s", err.Error())
|
||||
}
|
||||
|
||||
if len(cfg.Hosts) != 0 {
|
||||
for domain, ipStr := range cfg.Hosts {
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
ip, err := netip.ParseAddr(ipStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s is not a valid IP", ipStr)
|
||||
}
|
||||
_ = tree.Insert(domain, ip)
|
||||
}
|
||||
}
|
||||
|
||||
// add mitm.clash hosts
|
||||
if err := tree.Insert("mitm.clash", netip.AddrFrom4([4]byte{8, 8, 9, 9})); err != nil {
|
||||
log.Errorln("insert mitm.clash to host error: %s", err.Error())
|
||||
}
|
||||
|
||||
return tree, nil
|
||||
}
|
||||
|
||||
@ -697,7 +729,7 @@ func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]*router.DomainM
|
||||
return sites, nil
|
||||
}
|
||||
|
||||
func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS, error) {
|
||||
func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[netip.Addr], rules []C.Rule) (*DNS, error) {
|
||||
cfg := rawCfg.DNS
|
||||
if cfg.Enable && len(cfg.NameServer) == 0 {
|
||||
return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty")
|
||||
@ -750,10 +782,10 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var host *trie.DomainTrie
|
||||
var host *trie.DomainTrie[bool]
|
||||
// fake ip skip host filter
|
||||
if len(cfg.FakeIPFilter) != 0 {
|
||||
host = trie.New()
|
||||
host = trie.New[bool]()
|
||||
for _, domain := range cfg.FakeIPFilter {
|
||||
_ = host.Insert(domain, true)
|
||||
}
|
||||
@ -761,7 +793,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS,
|
||||
|
||||
if len(dnsCfg.Fallback) != 0 {
|
||||
if host == nil {
|
||||
host = trie.New()
|
||||
host = trie.New[bool]()
|
||||
}
|
||||
for _, fb := range dnsCfg.Fallback {
|
||||
if net.ParseIP(fb.Addr) != nil {
|
||||
@ -930,3 +962,38 @@ func cleanPyKeywords(code string) string {
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
func parseMitm(rawMitm RawMitm) (*Mitm, error) {
|
||||
var (
|
||||
req []C.Rewrite
|
||||
res []C.Rewrite
|
||||
)
|
||||
|
||||
for _, line := range rawMitm.Rules {
|
||||
rule, err := rewrites.ParseRewrite(line)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse rewrite rule failure: %w", err)
|
||||
}
|
||||
|
||||
if rule.RuleType() == C.MitmResponseHeader || rule.RuleType() == C.MitmResponseBody {
|
||||
res = append(res, rule)
|
||||
} else {
|
||||
req = append(req, rule)
|
||||
}
|
||||
}
|
||||
|
||||
hosts := trie.New[bool]()
|
||||
|
||||
if len(rawMitm.Hosts) != 0 {
|
||||
for _, domain := range rawMitm.Hosts {
|
||||
_ = hosts.Insert(domain, true)
|
||||
}
|
||||
}
|
||||
|
||||
_ = hosts.Insert("mitm.clash", true)
|
||||
|
||||
return &Mitm{
|
||||
Hosts: hosts,
|
||||
Rules: rewrites.NewRewriteRules(req, res),
|
||||
}, nil
|
||||
}
|
||||
|
@ -50,23 +50,6 @@ func initMMDB() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//func downloadGeoIP(path string) (err error) {
|
||||
// resp, err := http.Get("https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat")
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// defer resp.Body.Close()
|
||||
//
|
||||
// f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// defer f.Close()
|
||||
// _, err = io.Copy(f, resp.Body)
|
||||
//
|
||||
// return err
|
||||
//}
|
||||
|
||||
func downloadGeoSite(path string) (err error) {
|
||||
resp, err := http.Get("https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat")
|
||||
if err != nil {
|
||||
@ -84,19 +67,6 @@ func downloadGeoSite(path string) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
//
|
||||
//func initGeoIP() error {
|
||||
// if _, err := os.Stat(C.Path.GeoIP()); os.IsNotExist(err) {
|
||||
// log.Infoln("Can't find GeoIP.dat, start download")
|
||||
// if err := downloadGeoIP(C.Path.GeoIP()); err != nil {
|
||||
// return fmt.Errorf("can't download GeoIP.dat: %s", err.Error())
|
||||
// }
|
||||
// log.Infoln("Download GeoIP.dat finish")
|
||||
// }
|
||||
//
|
||||
// return nil
|
||||
//}
|
||||
|
||||
func initGeoSite() error {
|
||||
if _, err := os.Stat(C.Path.GeoSite()); os.IsNotExist(err) {
|
||||
log.Infoln("Can't find GeoSite.dat, start download")
|
||||
@ -129,11 +99,6 @@ func Init(dir string) error {
|
||||
f.Close()
|
||||
}
|
||||
|
||||
//// initial GeoIP
|
||||
//if err := initGeoIP(); err != nil {
|
||||
// return fmt.Errorf("can't initial GeoIP: %w", err)
|
||||
//}
|
||||
|
||||
// initial mmdb
|
||||
if err := initMMDB(); err != nil {
|
||||
return fmt.Errorf("can't initial MMDB: %w", err)
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
const (
|
||||
Direct AdapterType = iota
|
||||
Reject
|
||||
Mitm
|
||||
|
||||
Shadowsocks
|
||||
ShadowsocksR
|
||||
@ -129,6 +130,8 @@ func (at AdapterType) String() string {
|
||||
return "Direct"
|
||||
case Reject:
|
||||
return "Reject"
|
||||
case Mitm:
|
||||
return "Mitm"
|
||||
|
||||
case Shadowsocks:
|
||||
return "Shadowsocks"
|
||||
|
@ -23,6 +23,7 @@ const (
|
||||
REDIR
|
||||
TPROXY
|
||||
TUN
|
||||
MITM
|
||||
)
|
||||
|
||||
type NetWork int
|
||||
@ -58,6 +59,8 @@ func (t Type) String() string {
|
||||
return "TProxy"
|
||||
case TUN:
|
||||
return "Tun"
|
||||
case MITM:
|
||||
return "Mitm"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
@ -80,10 +83,15 @@ type Metadata struct {
|
||||
DNSMode DNSMode `json:"dnsMode"`
|
||||
Process string `json:"process"`
|
||||
ProcessPath string `json:"processPath"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
}
|
||||
|
||||
func (m *Metadata) RemoteAddress() string {
|
||||
return net.JoinHostPort(m.String(), m.DstPort)
|
||||
if m.DstIP != nil {
|
||||
return net.JoinHostPort(m.DstIP.String(), m.DstPort)
|
||||
} else {
|
||||
return net.JoinHostPort(m.String(), m.DstPort)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Metadata) SourceAddress() string {
|
||||
|
@ -98,6 +98,10 @@ func (p *path) GetExecutableFullPath() string {
|
||||
return res
|
||||
}
|
||||
|
||||
func (p *path) GetAssetLocation(file string) string {
|
||||
return P.Join(p.homeDir, file)
|
||||
func (p *path) RootCA() string {
|
||||
return p.Resolve("mitm_ca.crt")
|
||||
}
|
||||
|
||||
func (p *path) CAKey() string {
|
||||
return p.Resolve("mitm_ca.key")
|
||||
}
|
||||
|
82
constant/rewrite.go
Normal file
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
|
||||
}
|
@ -14,6 +14,7 @@ const (
|
||||
Process
|
||||
ProcessPath
|
||||
Script
|
||||
UserAgent
|
||||
MATCH
|
||||
)
|
||||
|
||||
@ -45,6 +46,8 @@ func (rt RuleType) String() string {
|
||||
return "ProcessPath"
|
||||
case Script:
|
||||
return "Script"
|
||||
case UserAgent:
|
||||
return "UserAgent"
|
||||
case MATCH:
|
||||
return "Match"
|
||||
default:
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
type ResolverEnhancer struct {
|
||||
mode C.DNSMode
|
||||
fakePool *fakeip.Pool
|
||||
mapping *cache.LruCache
|
||||
mapping *cache.LruCache[string, string]
|
||||
}
|
||||
|
||||
func (h *ResolverEnhancer) FakeIPEnabled() bool {
|
||||
@ -67,13 +67,19 @@ func (h *ResolverEnhancer) FindHostByIP(ip net.IP) (string, bool) {
|
||||
|
||||
if mapping := h.mapping; mapping != nil {
|
||||
if host, existed := h.mapping.Get(ip.String()); existed {
|
||||
return host.(string), true
|
||||
return host, true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (h *ResolverEnhancer) InsertHostByIP(ip net.IP, host string) {
|
||||
if mapping := h.mapping; mapping != nil {
|
||||
h.mapping.Set(ip.String(), host)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ResolverEnhancer) PatchFrom(o *ResolverEnhancer) {
|
||||
if h.mapping != nil && o.mapping != nil {
|
||||
o.mapping.CloneTo(h.mapping)
|
||||
@ -93,11 +99,11 @@ func (h *ResolverEnhancer) FlushFakeIP() error {
|
||||
|
||||
func NewEnhancer(cfg Config) *ResolverEnhancer {
|
||||
var fakePool *fakeip.Pool
|
||||
var mapping *cache.LruCache
|
||||
var mapping *cache.LruCache[string, string]
|
||||
|
||||
if cfg.EnhancedMode != C.DNSNormal {
|
||||
fakePool = cfg.Pool
|
||||
mapping = cache.NewLRUCache(cache.WithSize(4096), cache.WithStale(true))
|
||||
mapping = cache.NewLRUCache[string, string](cache.WithSize[string, string](4096), cache.WithStale[string, string](true))
|
||||
}
|
||||
|
||||
return &ResolverEnhancer{
|
||||
|
@ -35,13 +35,13 @@ type fallbackDomainFilter interface {
|
||||
}
|
||||
|
||||
type domainFilter struct {
|
||||
tree *trie.DomainTrie
|
||||
tree *trie.DomainTrie[bool]
|
||||
}
|
||||
|
||||
func NewDomainFilter(domains []string) *domainFilter {
|
||||
df := domainFilter{tree: trie.New()}
|
||||
df := domainFilter{tree: trie.New[bool]()}
|
||||
for _, domain := range domains {
|
||||
df.tree.Insert(domain, "")
|
||||
df.tree.Insert(domain, true)
|
||||
}
|
||||
return &df
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package dns
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -20,7 +21,7 @@ type (
|
||||
middleware func(next handler) handler
|
||||
)
|
||||
|
||||
func withHosts(hosts *trie.DomainTrie) middleware {
|
||||
func withHosts(hosts *trie.DomainTrie[netip.Addr], mapping *cache.LruCache[string, string]) middleware {
|
||||
return func(next handler) handler {
|
||||
return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) {
|
||||
q := r.Question[0]
|
||||
@ -29,24 +30,29 @@ func withHosts(hosts *trie.DomainTrie) middleware {
|
||||
return next(ctx, r)
|
||||
}
|
||||
|
||||
record := hosts.Search(strings.TrimRight(q.Name, "."))
|
||||
qName := strings.TrimRight(q.Name, ".")
|
||||
record := hosts.Search(qName)
|
||||
if record == nil {
|
||||
return next(ctx, r)
|
||||
}
|
||||
|
||||
ip := record.Data.(net.IP)
|
||||
ip := record.Data
|
||||
if mapping != nil {
|
||||
mapping.SetWithExpire(ip.Unmap().String(), qName, time.Now().Add(time.Second*5))
|
||||
}
|
||||
|
||||
msg := r.Copy()
|
||||
|
||||
if v4 := ip.To4(); v4 != nil && q.Qtype == D.TypeA {
|
||||
if ip.Is4() && q.Qtype == D.TypeA {
|
||||
rr := &D.A{}
|
||||
rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeA, Class: D.ClassINET, Ttl: dnsDefaultTTL}
|
||||
rr.A = v4
|
||||
rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeA, Class: D.ClassINET, Ttl: 1}
|
||||
rr.A = ip.AsSlice()
|
||||
|
||||
msg.Answer = []D.RR{rr}
|
||||
} else if v6 := ip.To16(); v6 != nil && q.Qtype == D.TypeAAAA {
|
||||
} else if ip.Is6() && q.Qtype == D.TypeAAAA {
|
||||
rr := &D.AAAA{}
|
||||
rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeAAAA, Class: D.ClassINET, Ttl: dnsDefaultTTL}
|
||||
rr.AAAA = v6
|
||||
rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeAAAA, Class: D.ClassINET, Ttl: 1}
|
||||
rr.AAAA = ip.AsSlice()
|
||||
|
||||
msg.Answer = []D.RR{rr}
|
||||
} else {
|
||||
@ -63,7 +69,7 @@ func withHosts(hosts *trie.DomainTrie) middleware {
|
||||
}
|
||||
}
|
||||
|
||||
func withMapping(mapping *cache.LruCache) middleware {
|
||||
func withMapping(mapping *cache.LruCache[string, string]) middleware {
|
||||
return func(next handler) handler {
|
||||
return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) {
|
||||
q := r.Question[0]
|
||||
@ -176,7 +182,7 @@ func NewHandler(resolver *Resolver, mapper *ResolverEnhancer) handler {
|
||||
middlewares := []middleware{}
|
||||
|
||||
if resolver.hosts != nil {
|
||||
middlewares = append(middlewares, withHosts(resolver.hosts))
|
||||
middlewares = append(middlewares, withHosts(resolver.hosts, mapper.mapping))
|
||||
}
|
||||
|
||||
if mapper.mode == C.DNSFakeIP {
|
||||
|
30
dns/policy.go
Normal file
30
dns/policy.go
Normal file
@ -0,0 +1,30 @@
|
||||
package dns
|
||||
|
||||
type Policy struct {
|
||||
data []dnsClient
|
||||
}
|
||||
|
||||
func (p *Policy) GetData() []dnsClient {
|
||||
return p.data
|
||||
}
|
||||
|
||||
func (p *Policy) Compare(p2 *Policy) int {
|
||||
if p2 == nil {
|
||||
return 1
|
||||
}
|
||||
l1 := len(p.data)
|
||||
l2 := len(p2.data)
|
||||
if l1 == l2 {
|
||||
return 0
|
||||
}
|
||||
if l1 > l2 {
|
||||
return 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func NewPolicy(data []dnsClient) *Policy {
|
||||
return &Policy{
|
||||
data: data,
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -33,14 +34,14 @@ type result struct {
|
||||
|
||||
type Resolver struct {
|
||||
ipv6 bool
|
||||
hosts *trie.DomainTrie
|
||||
hosts *trie.DomainTrie[netip.Addr]
|
||||
main []dnsClient
|
||||
fallback []dnsClient
|
||||
fallbackDomainFilters []fallbackDomainFilter
|
||||
fallbackIPFilters []fallbackIPFilter
|
||||
group singleflight.Group
|
||||
lruCache *cache.LruCache
|
||||
policy *trie.DomainTrie
|
||||
lruCache *cache.LruCache[string, *D.Msg]
|
||||
policy *trie.DomainTrie[*Policy]
|
||||
proxyServer []dnsClient
|
||||
}
|
||||
|
||||
@ -103,7 +104,7 @@ func (r *Resolver) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, e
|
||||
cache, expireTime, hit := r.lruCache.GetWithExpire(q.String())
|
||||
if hit {
|
||||
now := time.Now()
|
||||
msg = cache.(*D.Msg).Copy()
|
||||
msg = cache.Copy()
|
||||
if expireTime.Before(now) {
|
||||
setMsgTTL(msg, uint32(1)) // Continue fetch
|
||||
go r.exchangeWithoutCache(ctx, m)
|
||||
@ -194,7 +195,8 @@ func (r *Resolver) matchPolicy(m *D.Msg) []dnsClient {
|
||||
return nil
|
||||
}
|
||||
|
||||
return record.Data.([]dnsClient)
|
||||
p := record.Data
|
||||
return p.GetData()
|
||||
}
|
||||
|
||||
func (r *Resolver) shouldOnlyQueryFallback(m *D.Msg) bool {
|
||||
@ -330,20 +332,20 @@ type Config struct {
|
||||
EnhancedMode C.DNSMode
|
||||
FallbackFilter FallbackFilter
|
||||
Pool *fakeip.Pool
|
||||
Hosts *trie.DomainTrie
|
||||
Hosts *trie.DomainTrie[netip.Addr]
|
||||
Policy map[string]NameServer
|
||||
}
|
||||
|
||||
func NewResolver(config Config) *Resolver {
|
||||
defaultResolver := &Resolver{
|
||||
main: transform(config.Default, nil),
|
||||
lruCache: cache.NewLRUCache(cache.WithSize(4096), cache.WithStale(true)),
|
||||
lruCache: cache.NewLRUCache[string, *D.Msg](cache.WithSize[string, *D.Msg](4096), cache.WithStale[string, *D.Msg](true)),
|
||||
}
|
||||
|
||||
r := &Resolver{
|
||||
ipv6: config.IPv6,
|
||||
main: transform(config.Main, defaultResolver),
|
||||
lruCache: cache.NewLRUCache(cache.WithSize(4096), cache.WithStale(true)),
|
||||
lruCache: cache.NewLRUCache[string, *D.Msg](cache.WithSize[string, *D.Msg](4096), cache.WithStale[string, *D.Msg](true)),
|
||||
hosts: config.Hosts,
|
||||
}
|
||||
|
||||
@ -356,9 +358,9 @@ func NewResolver(config Config) *Resolver {
|
||||
}
|
||||
|
||||
if len(config.Policy) != 0 {
|
||||
r.policy = trie.New()
|
||||
r.policy = trie.New[*Policy]()
|
||||
for domain, nameserver := range config.Policy {
|
||||
r.policy.Insert(domain, transform([]NameServer{nameserver}, defaultResolver))
|
||||
r.policy.Insert(domain, NewPolicy(transform([]NameServer{nameserver}, defaultResolver)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@ import (
|
||||
D "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func putMsgToCache(c *cache.LruCache, key string, msg *D.Msg) {
|
||||
func putMsgToCache(c *cache.LruCache[string, *D.Msg], key string, msg *D.Msg) {
|
||||
var ttl uint32
|
||||
switch {
|
||||
case len(msg.Answer) != 0:
|
||||
|
10
go.mod
10
go.mod
@ -18,10 +18,11 @@ require (
|
||||
go.etcd.io/bbolt v1.3.6
|
||||
go.uber.org/atomic v1.9.0
|
||||
go.uber.org/automaxprocs v1.4.0
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f
|
||||
golang.org/x/crypto v0.0.0-20220408190544-5352b0902921
|
||||
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f
|
||||
golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab
|
||||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65
|
||||
golang.zx2c4.com/wireguard v0.0.0-20220318042302-193cf8d6a5d6
|
||||
golang.zx2c4.com/wireguard/windows v0.5.4-0.20220317000008-6432784c2469
|
||||
@ -37,8 +38,7 @@ require (
|
||||
github.com/oschwald/maxminddb-golang v1.8.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect
|
||||
golang.org/x/mod v0.5.1 // indirect
|
||||
golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 // indirect
|
||||
golang.org/x/tools v0.1.9 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
|
||||
|
16
go.sum
16
go.sum
@ -81,11 +81,11 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38=
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 h1:iU7T1X1J6yxDr0rda54sWGkHgOp5XJrqm79gcNlC2VM=
|
||||
golang.org/x/crypto v0.0.0-20220408190544-5352b0902921/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 h1:LQmS1nU0twXLA96Kt7U9qtHJEbBk3z6Q0V4UXjZkpr4=
|
||||
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@ -99,8 +99,8 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 h1:EN5+DfgmRMvRUrMGERW2gQl3Vc+Z7ZMnI/xdEpPSf0c=
|
||||
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -125,8 +125,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 h1:A9i04dxx7Cribqbs8jf3FQLogkL/CV2YN7hj9KWJCkc=
|
||||
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw=
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
@ -3,12 +3,14 @@ package executor
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/Dreamacro/clash/adapter"
|
||||
"github.com/Dreamacro/clash/adapter/outbound"
|
||||
"github.com/Dreamacro/clash/adapter/outboundgroup"
|
||||
"github.com/Dreamacro/clash/component/auth"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
@ -72,13 +74,18 @@ func ApplyConfig(cfg *config.Config, force bool) {
|
||||
mux.Lock()
|
||||
defer mux.Unlock()
|
||||
|
||||
log.SetLevel(log.DEBUG)
|
||||
if cfg.General.LogLevel == log.DEBUG {
|
||||
log.SetLevel(log.DEBUG)
|
||||
} else {
|
||||
log.SetLevel(log.INFO)
|
||||
}
|
||||
|
||||
updateUsers(cfg.Users)
|
||||
updateProxies(cfg.Proxies, cfg.Providers)
|
||||
updateRules(cfg.Rules)
|
||||
updateRuleProviders(cfg.RuleProviders)
|
||||
updateHosts(cfg.Hosts)
|
||||
updateMitm(cfg.Mitm)
|
||||
updateProfile(cfg)
|
||||
updateDNS(cfg.DNS, cfg.Tun)
|
||||
updateGeneral(cfg.General, force)
|
||||
@ -103,6 +110,7 @@ func GetGeneral() *config.General {
|
||||
RedirPort: ports.RedirPort,
|
||||
TProxyPort: ports.TProxyPort,
|
||||
MixedPort: ports.MixedPort,
|
||||
MitmPort: ports.MitmPort,
|
||||
Authentication: authenticator,
|
||||
AllowLan: P.AllowLan(),
|
||||
BindAddress: P.BindAddress(),
|
||||
@ -170,7 +178,7 @@ func updateDNS(c *config.DNS, t *config.Tun) {
|
||||
}
|
||||
}
|
||||
|
||||
func updateHosts(tree *trie.DomainTrie) {
|
||||
func updateHosts(tree *trie.DomainTrie[netip.Addr]) {
|
||||
resolver.DefaultHosts = tree
|
||||
}
|
||||
|
||||
@ -231,6 +239,7 @@ func updateGeneral(general *config.General, force bool) {
|
||||
P.ReCreateRedir(general.RedirPort, tcpIn, udpIn)
|
||||
P.ReCreateTProxy(general.TProxyPort, tcpIn, udpIn)
|
||||
P.ReCreateMixed(general.MixedPort, tcpIn, udpIn)
|
||||
P.ReCreateMitm(general.MitmPort, tcpIn)
|
||||
}
|
||||
|
||||
func updateUsers(users []auth.AuthUser) {
|
||||
@ -336,6 +345,11 @@ func updateIPTables(cfg *config.Config) {
|
||||
log.Infoln("[IPTABLES] Setting iptables completed")
|
||||
}
|
||||
|
||||
func updateMitm(mitm *config.Mitm) {
|
||||
outbound.MiddlemanRewriteHosts = mitm.Hosts
|
||||
tunnel.UpdateRewrites(mitm.Rules)
|
||||
}
|
||||
|
||||
func Shutdown() {
|
||||
P.Cleanup()
|
||||
S.Py_Finalize()
|
||||
|
@ -30,6 +30,7 @@ type configSchema struct {
|
||||
RedirPort *int `json:"redir-port"`
|
||||
TProxyPort *int `json:"tproxy-port"`
|
||||
MixedPort *int `json:"mixed-port"`
|
||||
MitmPort *int `json:"mitm-port"`
|
||||
Tun *config.Tun `json:"tun"`
|
||||
AllowLan *bool `json:"allow-lan"`
|
||||
BindAddress *string `json:"bind-address"`
|
||||
@ -77,6 +78,7 @@ func patchConfigs(w http.ResponseWriter, r *http.Request) {
|
||||
P.ReCreateRedir(pointerOrDefault(general.RedirPort, ports.RedirPort), tcpIn, udpIn)
|
||||
P.ReCreateTProxy(pointerOrDefault(general.TProxyPort, ports.TProxyPort), tcpIn, udpIn)
|
||||
P.ReCreateMixed(pointerOrDefault(general.MixedPort, ports.MixedPort), tcpIn, udpIn)
|
||||
P.ReCreateMitm(pointerOrDefault(general.MitmPort, ports.MitmPort), tcpIn)
|
||||
|
||||
if general.Mode != nil {
|
||||
tunnel.SetMode(*general.Mode)
|
||||
|
@ -15,7 +15,7 @@ import (
|
||||
"github.com/Dreamacro/clash/log"
|
||||
)
|
||||
|
||||
func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.Cache) {
|
||||
func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.Cache[string, bool]) {
|
||||
client := newClient(c.RemoteAddr(), in)
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
@ -42,7 +42,7 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.Cache) {
|
||||
var resp *http.Response
|
||||
|
||||
if !trusted {
|
||||
resp = authenticate(request, cache)
|
||||
resp = Authenticate(request, cache)
|
||||
|
||||
trusted = resp == nil
|
||||
}
|
||||
@ -66,19 +66,19 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.Cache) {
|
||||
|
||||
request.RequestURI = ""
|
||||
|
||||
removeHopByHopHeaders(request.Header)
|
||||
removeExtraHTTPHostPort(request)
|
||||
RemoveHopByHopHeaders(request.Header)
|
||||
RemoveExtraHTTPHostPort(request)
|
||||
|
||||
if request.URL.Scheme == "" || request.URL.Host == "" {
|
||||
resp = responseWith(request, http.StatusBadRequest)
|
||||
resp = ResponseWith(request, http.StatusBadRequest)
|
||||
} else {
|
||||
resp, err = client.Do(request)
|
||||
if err != nil {
|
||||
resp = responseWith(request, http.StatusBadGateway)
|
||||
resp = ResponseWith(request, http.StatusBadGateway)
|
||||
}
|
||||
}
|
||||
|
||||
removeHopByHopHeaders(resp.Header)
|
||||
RemoveHopByHopHeaders(resp.Header)
|
||||
}
|
||||
|
||||
if keepAlive {
|
||||
@ -98,33 +98,33 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.Cache) {
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
func authenticate(request *http.Request, cache *cache.Cache) *http.Response {
|
||||
func Authenticate(request *http.Request, cache *cache.Cache[string, bool]) *http.Response {
|
||||
authenticator := authStore.Authenticator()
|
||||
if authenticator != nil {
|
||||
credential := parseBasicProxyAuthorization(request)
|
||||
if credential == "" {
|
||||
resp := responseWith(request, http.StatusProxyAuthRequired)
|
||||
resp := ResponseWith(request, http.StatusProxyAuthRequired)
|
||||
resp.Header.Set("Proxy-Authenticate", "Basic")
|
||||
return resp
|
||||
}
|
||||
|
||||
var authed any
|
||||
if authed = cache.Get(credential); authed == nil {
|
||||
var authed bool
|
||||
if authed = cache.Get(credential); !authed {
|
||||
user, pass, err := decodeBasicProxyAuthorization(credential)
|
||||
authed = err == nil && authenticator.Verify(user, pass)
|
||||
cache.Put(credential, authed, time.Minute)
|
||||
}
|
||||
if !authed.(bool) {
|
||||
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),
|
||||
|
@ -40,9 +40,9 @@ func NewWithAuthenticate(addr string, in chan<- C.ConnContext, authenticate bool
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var c *cache.Cache
|
||||
var c *cache.Cache[string, bool]
|
||||
if authenticate {
|
||||
c = cache.New(time.Second * 30)
|
||||
c = cache.New[string, bool](time.Second * 30)
|
||||
}
|
||||
|
||||
hl := &Listener{
|
||||
|
@ -8,8 +8,8 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// removeHopByHopHeaders remove hop-by-hop header
|
||||
func removeHopByHopHeaders(header http.Header) {
|
||||
// RemoveHopByHopHeaders remove hop-by-hop header
|
||||
func RemoveHopByHopHeaders(header http.Header) {
|
||||
// Strip hop-by-hop header based on RFC:
|
||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
|
||||
// https://www.mnot.net/blog/2011/07/11/what_proxies_must_do
|
||||
@ -32,9 +32,9 @@ func removeHopByHopHeaders(header http.Header) {
|
||||
}
|
||||
}
|
||||
|
||||
// removeExtraHTTPHostPort remove extra host port (example.com:80 --> example.com)
|
||||
// RemoveExtraHTTPHostPort remove extra host port (example.com:80 --> example.com)
|
||||
// It resolves the behavior of some HTTP servers that do not handle host:80 (e.g. baidu.com)
|
||||
func removeExtraHTTPHostPort(req *http.Request) {
|
||||
func RemoveExtraHTTPHostPort(req *http.Request) {
|
||||
host := req.Host
|
||||
if host == "" {
|
||||
host = req.URL.Host
|
||||
|
@ -1,6 +1,9 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
@ -8,10 +11,13 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/Dreamacro/clash/adapter/inbound"
|
||||
"github.com/Dreamacro/clash/adapter/outbound"
|
||||
"github.com/Dreamacro/clash/common/cert"
|
||||
S "github.com/Dreamacro/clash/component/script"
|
||||
"github.com/Dreamacro/clash/config"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/listener/http"
|
||||
"github.com/Dreamacro/clash/listener/mitm"
|
||||
"github.com/Dreamacro/clash/listener/mixed"
|
||||
"github.com/Dreamacro/clash/listener/redir"
|
||||
"github.com/Dreamacro/clash/listener/socks"
|
||||
@ -19,6 +25,8 @@ import (
|
||||
"github.com/Dreamacro/clash/listener/tun"
|
||||
"github.com/Dreamacro/clash/listener/tun/ipstack"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
rewrites "github.com/Dreamacro/clash/rewrite"
|
||||
"github.com/Dreamacro/clash/tunnel"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -35,6 +43,7 @@ var (
|
||||
mixedListener *mixed.Listener
|
||||
mixedUDPLister *socks.UDPListener
|
||||
tunStackListener ipstack.Stack
|
||||
mitmListener *mitm.Listener
|
||||
|
||||
// lock for recreate function
|
||||
socksMux sync.Mutex
|
||||
@ -43,6 +52,7 @@ var (
|
||||
tproxyMux sync.Mutex
|
||||
mixedMux sync.Mutex
|
||||
tunMux sync.Mutex
|
||||
mitmMux sync.Mutex
|
||||
)
|
||||
|
||||
type Ports struct {
|
||||
@ -51,6 +61,7 @@ type Ports struct {
|
||||
RedirPort int `json:"redir-port"`
|
||||
TProxyPort int `json:"tproxy-port"`
|
||||
MixedPort int `json:"mixed-port"`
|
||||
MitmPort int `json:"mitm-port"`
|
||||
}
|
||||
|
||||
func AllowLan() bool {
|
||||
@ -333,6 +344,85 @@ func ReCreateTun(tunConf *config.Tun, tunAddressPrefix string, tcpIn chan<- C.Co
|
||||
tunStackListener, err = tun.New(tunConf, tunAddressPrefix, tcpIn, udpIn)
|
||||
}
|
||||
|
||||
func ReCreateMitm(port int, tcpIn chan<- C.ConnContext) {
|
||||
mitmMux.Lock()
|
||||
defer mitmMux.Unlock()
|
||||
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.Errorln("Start MITM server error: %s", err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
addr := genAddr(bindAddress, port, allowLan)
|
||||
|
||||
if mitmListener != nil {
|
||||
if mitmListener.RawAddress() == addr {
|
||||
return
|
||||
}
|
||||
outbound.MiddlemanServerAddress.Store("")
|
||||
tunnel.MitmOutbound = nil
|
||||
_ = mitmListener.Close()
|
||||
mitmListener = nil
|
||||
}
|
||||
|
||||
if portIsZero(addr) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = initCert(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
rootCACert tls.Certificate
|
||||
x509c *x509.Certificate
|
||||
certOption *cert.Config
|
||||
)
|
||||
|
||||
rootCACert, err = tls.LoadX509KeyPair(C.Path.RootCA(), C.Path.CAKey())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
privateKey := rootCACert.PrivateKey.(*rsa.PrivateKey)
|
||||
|
||||
x509c, err = x509.ParseCertificate(rootCACert.Certificate[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
certOption, err = cert.NewConfig(
|
||||
x509c,
|
||||
privateKey,
|
||||
cert.NewAutoGCCertsStorage(),
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
certOption.SetValidity(cert.TTL << 3)
|
||||
certOption.SetOrganization("Clash ManInTheMiddle Proxy Services")
|
||||
|
||||
opt := &mitm.Option{
|
||||
Addr: addr,
|
||||
ApiHost: "mitm.clash",
|
||||
CertConfig: certOption,
|
||||
Handler: &rewrites.RewriteHandler{},
|
||||
}
|
||||
|
||||
mitmListener, err = mitm.New(opt, tcpIn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
outbound.MiddlemanServerAddress.Store(mitmListener.Address())
|
||||
tunnel.MitmOutbound = outbound.NewMitm()
|
||||
|
||||
log.Infoln("Mitm proxy listening at: %s", mitmListener.Address())
|
||||
}
|
||||
|
||||
// GetPorts return the ports of proxy servers
|
||||
func GetPorts() *Ports {
|
||||
ports := &Ports{}
|
||||
@ -367,6 +457,12 @@ func GetPorts() *Ports {
|
||||
ports.MixedPort = port
|
||||
}
|
||||
|
||||
if mitmListener != nil {
|
||||
_, portStr, _ := net.SplitHostPort(mitmListener.Address())
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
ports.MitmPort = port
|
||||
}
|
||||
|
||||
return ports
|
||||
}
|
||||
|
||||
@ -389,6 +485,19 @@ func genAddr(host string, port int, allowLan bool) string {
|
||||
return fmt.Sprintf("127.0.0.1:%d", port)
|
||||
}
|
||||
|
||||
func initCert() error {
|
||||
if _, err := os.Stat(C.Path.RootCA()); os.IsNotExist(err) {
|
||||
log.Infoln("Can't find mitm_ca.crt, start generate")
|
||||
err = cert.GenerateAndSave(C.Path.RootCA(), C.Path.CAKey())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infoln("Generated CA private key and CA certificate finish")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Cleanup() {
|
||||
if tunStackListener != nil {
|
||||
_ = tunStackListener.Close()
|
||||
|
54
listener/mitm/client.go
Normal file
54
listener/mitm/client.go
Normal file
@ -0,0 +1,54 @@
|
||||
package mitm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/adapter/inbound"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/transport/socks5"
|
||||
)
|
||||
|
||||
var ErrCertUnsupported = errors.New("tls: client cert unsupported")
|
||||
|
||||
func newClient(source net.Addr, userAgent string, in chan<- C.ConnContext) *http.Client {
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
// excepted HTTP/2
|
||||
TLSNextProto: make(map[string]func(string, *tls.Conn) http.RoundTripper),
|
||||
// from http.DefaultTransport
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
TLSClientConfig: &tls.Config{
|
||||
GetClientCertificate: func(info *tls.CertificateRequestInfo) (certificate *tls.Certificate, e error) {
|
||||
return nil, ErrCertUnsupported
|
||||
},
|
||||
},
|
||||
DialContext: func(context context.Context, network, address string) (net.Conn, error) {
|
||||
if network != "tcp" && network != "tcp4" && network != "tcp6" {
|
||||
return nil, errors.New("unsupported network " + network)
|
||||
}
|
||||
|
||||
dstAddr := socks5.ParseAddr(address)
|
||||
if dstAddr == nil {
|
||||
return nil, socks5.ErrAddressNotSupported
|
||||
}
|
||||
|
||||
left, right := net.Pipe()
|
||||
|
||||
in <- inbound.NewMitm(dstAddr, source, userAgent, right)
|
||||
|
||||
return left, nil
|
||||
},
|
||||
},
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
}
|
357
listener/mitm/proxy.go
Normal file
357
listener/mitm/proxy.go
Normal file
@ -0,0 +1,357 @@
|
||||
package mitm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/adapter/inbound"
|
||||
"github.com/Dreamacro/clash/common/cache"
|
||||
N "github.com/Dreamacro/clash/common/net"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
httpL "github.com/Dreamacro/clash/listener/http"
|
||||
)
|
||||
|
||||
func HandleConn(c net.Conn, opt *Option, in chan<- C.ConnContext, cache *cache.Cache[string, bool]) {
|
||||
var (
|
||||
source net.Addr
|
||||
client *http.Client
|
||||
)
|
||||
|
||||
defer func() {
|
||||
if client != nil {
|
||||
client.CloseIdleConnections()
|
||||
}
|
||||
}()
|
||||
|
||||
startOver:
|
||||
if tc, ok := c.(*net.TCPConn); ok {
|
||||
_ = tc.SetKeepAlive(true)
|
||||
}
|
||||
|
||||
var conn *N.BufferedConn
|
||||
if bufConn, ok := c.(*N.BufferedConn); ok {
|
||||
conn = bufConn
|
||||
} else {
|
||||
conn = N.NewBufferedConn(c)
|
||||
}
|
||||
|
||||
trusted := cache == nil // disable authenticate if cache is nil
|
||||
|
||||
readLoop:
|
||||
for {
|
||||
_ = conn.SetDeadline(time.Now().Add(30 * time.Second)) // use SetDeadline instead of Proxy-Connection keep-alive
|
||||
|
||||
request, err := httpL.ReadRequest(conn.Reader())
|
||||
if err != nil {
|
||||
handleError(opt, nil, err)
|
||||
break readLoop
|
||||
}
|
||||
|
||||
var response *http.Response
|
||||
|
||||
session := NewSession(conn, request, response)
|
||||
|
||||
source = parseSourceAddress(session.request, c, source)
|
||||
request.RemoteAddr = source.String()
|
||||
|
||||
if !trusted {
|
||||
response = httpL.Authenticate(request, cache)
|
||||
|
||||
trusted = response == nil
|
||||
}
|
||||
|
||||
if trusted {
|
||||
if session.request.Method == http.MethodConnect {
|
||||
// Manual writing to support CONNECT for http 1.0 (workaround for uplay client)
|
||||
if _, err = fmt.Fprintf(session.conn, "HTTP/%d.%d %03d %s\r\n\r\n", session.request.ProtoMajor, session.request.ProtoMinor, http.StatusOK, "Connection established"); err != nil {
|
||||
handleError(opt, session, err)
|
||||
break readLoop // close connection
|
||||
}
|
||||
|
||||
if couldBeWithManInTheMiddleAttack(session.request.URL.Host, opt) {
|
||||
b := make([]byte, 1)
|
||||
if _, err = session.conn.Read(b); err != nil {
|
||||
handleError(opt, session, err)
|
||||
break readLoop // close connection
|
||||
}
|
||||
|
||||
buf := make([]byte, session.conn.(*N.BufferedConn).Buffered())
|
||||
_, _ = session.conn.Read(buf)
|
||||
|
||||
mc := &MultiReaderConn{
|
||||
Conn: session.conn,
|
||||
reader: io.MultiReader(bytes.NewReader(b), bytes.NewReader(buf), session.conn),
|
||||
}
|
||||
|
||||
// 22 is the TLS handshake.
|
||||
// https://tools.ietf.org/html/rfc5246#section-6.2.1
|
||||
if b[0] == 22 {
|
||||
// TODO serve by generic host name maybe better?
|
||||
tlsConn := tls.Server(mc, opt.CertConfig.NewTLSConfigForHost(session.request.URL.Host))
|
||||
|
||||
// Handshake with the local client
|
||||
if err = tlsConn.Handshake(); err != nil {
|
||||
handleError(opt, session, err)
|
||||
break readLoop // close connection
|
||||
}
|
||||
|
||||
c = tlsConn
|
||||
goto startOver // hijack and decrypt tls connection
|
||||
}
|
||||
|
||||
// maybe it's the others encrypted connection
|
||||
in <- inbound.NewHTTPS(request, mc)
|
||||
}
|
||||
|
||||
// maybe it's a http connection
|
||||
goto readLoop
|
||||
}
|
||||
|
||||
// hijack api
|
||||
if getHostnameWithoutPort(session.request) == opt.ApiHost {
|
||||
if err = handleApiRequest(session, opt); err != nil {
|
||||
handleError(opt, session, err)
|
||||
break readLoop
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
prepareRequest(c, session.request)
|
||||
|
||||
// hijack custom request and write back custom response if necessary
|
||||
if opt.Handler != nil {
|
||||
newReq, newRes := opt.Handler.HandleRequest(session)
|
||||
if newReq != nil {
|
||||
session.request = newReq
|
||||
}
|
||||
if newRes != nil {
|
||||
session.response = newRes
|
||||
|
||||
if err = writeResponse(session, false); err != nil {
|
||||
handleError(opt, session, err)
|
||||
break readLoop
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
httpL.RemoveHopByHopHeaders(session.request.Header)
|
||||
httpL.RemoveExtraHTTPHostPort(request)
|
||||
|
||||
session.request.RequestURI = ""
|
||||
|
||||
if session.request.URL.Scheme == "" || session.request.URL.Host == "" {
|
||||
session.response = session.NewErrorResponse(errors.New("invalid URL"))
|
||||
} else {
|
||||
client = newClientBySourceAndUserAgentIfNil(client, session.request, source, in)
|
||||
|
||||
// send the request to remote server
|
||||
session.response, err = client.Do(session.request)
|
||||
|
||||
if err != nil {
|
||||
handleError(opt, session, err)
|
||||
session.response = session.NewErrorResponse(err)
|
||||
if errors.Is(err, ErrCertUnsupported) || strings.Contains(err.Error(), "x509: ") {
|
||||
// TODO block unsupported host?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err = writeResponseWithHandler(session, opt); err != nil {
|
||||
handleError(opt, session, err)
|
||||
break readLoop // close connection
|
||||
}
|
||||
}
|
||||
|
||||
_ = conn.Close()
|
||||
}
|
||||
|
||||
func writeResponseWithHandler(session *Session, opt *Option) error {
|
||||
if opt.Handler != nil {
|
||||
res := opt.Handler.HandleResponse(session)
|
||||
|
||||
if res != nil {
|
||||
body := res.Body
|
||||
defer func(body io.ReadCloser) {
|
||||
_ = body.Close()
|
||||
}(body)
|
||||
|
||||
session.response = res
|
||||
}
|
||||
}
|
||||
|
||||
return writeResponse(session, true)
|
||||
}
|
||||
|
||||
func writeResponse(session *Session, keepAlive bool) error {
|
||||
httpL.RemoveHopByHopHeaders(session.response.Header)
|
||||
|
||||
if keepAlive {
|
||||
session.response.Header.Set("Connection", "keep-alive")
|
||||
session.response.Header.Set("Keep-Alive", "timeout=25")
|
||||
}
|
||||
|
||||
// session.response.Close = !keepAlive // let handler do it
|
||||
|
||||
return session.response.Write(session.conn)
|
||||
}
|
||||
|
||||
func handleApiRequest(session *Session, opt *Option) error {
|
||||
if opt.CertConfig != nil && strings.ToLower(session.request.URL.Path) == "/cert.crt" {
|
||||
b := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: opt.CertConfig.GetCA().Raw,
|
||||
})
|
||||
|
||||
session.response = session.NewResponse(http.StatusOK, bytes.NewReader(b))
|
||||
|
||||
defer func(body io.ReadCloser) {
|
||||
_ = body.Close()
|
||||
}(session.response.Body)
|
||||
|
||||
session.response.Close = true
|
||||
session.response.Header.Set("Content-Type", "application/x-x509-ca-cert")
|
||||
session.response.ContentLength = int64(len(b))
|
||||
|
||||
return session.response.Write(session.conn)
|
||||
}
|
||||
|
||||
b := `<!DOCTYPE HTML PUBLIC "-
|
||||
<html>
|
||||
<head>
|
||||
<title>Clash ManInTheMiddle Proxy Services - 404 Not Found</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Not Found</h1>
|
||||
<p>The requested URL %s was not found on this server.</p>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
if opt.Handler != nil {
|
||||
if opt.Handler.HandleApiRequest(session) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
b = fmt.Sprintf(b, session.request.URL.Path)
|
||||
|
||||
session.response = session.NewResponse(http.StatusNotFound, bytes.NewReader([]byte(b)))
|
||||
|
||||
defer func(body io.ReadCloser) {
|
||||
_ = body.Close()
|
||||
}(session.response.Body)
|
||||
|
||||
session.response.Close = true
|
||||
session.response.Header.Set("Content-Type", "text/html;charset=utf-8")
|
||||
session.response.ContentLength = int64(len(b))
|
||||
|
||||
return session.response.Write(session.conn)
|
||||
}
|
||||
|
||||
func handleError(opt *Option, session *Session, err error) {
|
||||
if opt.Handler != nil {
|
||||
opt.Handler.HandleError(session, err)
|
||||
return
|
||||
}
|
||||
|
||||
// log.Errorln("[MITM] process mitm error: %v", err)
|
||||
}
|
||||
|
||||
func prepareRequest(conn net.Conn, request *http.Request) {
|
||||
host := request.Header.Get("Host")
|
||||
if host != "" {
|
||||
request.Host = host
|
||||
}
|
||||
|
||||
if request.URL.Host == "" {
|
||||
request.URL.Host = request.Host
|
||||
}
|
||||
|
||||
request.URL.Scheme = "http"
|
||||
|
||||
if tlsConn, ok := conn.(*tls.Conn); ok {
|
||||
cs := tlsConn.ConnectionState()
|
||||
request.TLS = &cs
|
||||
|
||||
request.URL.Scheme = "https"
|
||||
}
|
||||
|
||||
if request.Header.Get("Accept-Encoding") != "" {
|
||||
request.Header.Set("Accept-Encoding", "gzip")
|
||||
}
|
||||
}
|
||||
|
||||
func couldBeWithManInTheMiddleAttack(hostname string, opt *Option) bool {
|
||||
if opt.CertConfig == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, port, err := net.SplitHostPort(hostname); err == nil && (port == "443" || port == "8443") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func getHostnameWithoutPort(req *http.Request) string {
|
||||
host := req.Host
|
||||
if host == "" {
|
||||
host = req.URL.Host
|
||||
}
|
||||
|
||||
if pHost, _, err := net.SplitHostPort(host); err == nil {
|
||||
host = pHost
|
||||
}
|
||||
|
||||
return host
|
||||
}
|
||||
|
||||
func parseSourceAddress(req *http.Request, c net.Conn, source net.Addr) net.Addr {
|
||||
if source != nil {
|
||||
return source
|
||||
}
|
||||
|
||||
sourceAddress := req.Header.Get("Origin-Request-Source-Address")
|
||||
if sourceAddress == "" {
|
||||
return c.RemoteAddr()
|
||||
}
|
||||
|
||||
req.Header.Del("Origin-Request-Source-Address")
|
||||
|
||||
host, port, err := net.SplitHostPort(sourceAddress)
|
||||
if err != nil {
|
||||
return c.RemoteAddr()
|
||||
}
|
||||
|
||||
p, err := strconv.ParseUint(port, 10, 16)
|
||||
if err != nil {
|
||||
return c.RemoteAddr()
|
||||
}
|
||||
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
return &net.TCPAddr{
|
||||
IP: ip,
|
||||
Port: int(p),
|
||||
}
|
||||
}
|
||||
|
||||
return c.RemoteAddr()
|
||||
}
|
||||
|
||||
func newClientBySourceAndUserAgentIfNil(cli *http.Client, req *http.Request, source net.Addr, in chan<- C.ConnContext) *http.Client {
|
||||
if cli != nil {
|
||||
return cli
|
||||
}
|
||||
|
||||
return newClient(source, req.Header.Get("User-Agent"), in)
|
||||
}
|
90
listener/mitm/server.go
Normal file
90
listener/mitm/server.go
Normal file
@ -0,0 +1,90 @@
|
||||
package mitm
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/common/cache"
|
||||
"github.com/Dreamacro/clash/common/cert"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type Handler interface {
|
||||
HandleRequest(*Session) (*http.Request, *http.Response) // Session.Response maybe nil
|
||||
HandleResponse(*Session) *http.Response
|
||||
HandleApiRequest(*Session) bool
|
||||
HandleError(*Session, error) // Session maybe nil
|
||||
}
|
||||
|
||||
type Option struct {
|
||||
Addr string
|
||||
ApiHost string
|
||||
|
||||
TLSConfig *tls.Config
|
||||
CertConfig *cert.Config
|
||||
|
||||
Handler Handler
|
||||
}
|
||||
|
||||
type Listener struct {
|
||||
*Option
|
||||
|
||||
listener net.Listener
|
||||
addr string
|
||||
closed bool
|
||||
}
|
||||
|
||||
// RawAddress implements C.Listener
|
||||
func (l *Listener) RawAddress() string {
|
||||
return l.addr
|
||||
}
|
||||
|
||||
// Address implements C.Listener
|
||||
func (l *Listener) Address() string {
|
||||
return l.listener.Addr().String()
|
||||
}
|
||||
|
||||
// Close implements C.Listener
|
||||
func (l *Listener) Close() error {
|
||||
l.closed = true
|
||||
return l.listener.Close()
|
||||
}
|
||||
|
||||
// New the MITM proxy actually is a type of HTTP proxy
|
||||
func New(option *Option, in chan<- C.ConnContext) (*Listener, error) {
|
||||
return NewWithAuthenticate(option, in, false)
|
||||
}
|
||||
|
||||
func NewWithAuthenticate(option *Option, in chan<- C.ConnContext, authenticate bool) (*Listener, error) {
|
||||
l, err := net.Listen("tcp", option.Addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var c *cache.Cache[string, bool]
|
||||
if authenticate {
|
||||
c = cache.New[string, bool](time.Second * 30)
|
||||
}
|
||||
|
||||
hl := &Listener{
|
||||
listener: l,
|
||||
addr: option.Addr,
|
||||
Option: option,
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
conn, err1 := hl.listener.Accept()
|
||||
if err1 != nil {
|
||||
if hl.closed {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
go HandleConn(conn, option, in, c)
|
||||
}
|
||||
}()
|
||||
|
||||
return hl, nil
|
||||
}
|
56
listener/mitm/session.go
Normal file
56
listener/mitm/session.go
Normal file
@ -0,0 +1,56 @@
|
||||
package mitm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
var serverName = fmt.Sprintf("Clash server (%s)", C.Version)
|
||||
|
||||
type Session struct {
|
||||
conn net.Conn
|
||||
request *http.Request
|
||||
response *http.Response
|
||||
|
||||
props map[string]any
|
||||
}
|
||||
|
||||
func (s *Session) Request() *http.Request {
|
||||
return s.request
|
||||
}
|
||||
|
||||
func (s *Session) Response() *http.Response {
|
||||
return s.response
|
||||
}
|
||||
|
||||
func (s *Session) GetProperties(key string) (any, bool) {
|
||||
v, ok := s.props[key]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (s *Session) SetProperties(key string, val any) {
|
||||
s.props[key] = val
|
||||
}
|
||||
|
||||
func (s *Session) NewResponse(code int, body io.Reader) *http.Response {
|
||||
res := NewResponse(code, body, s.request)
|
||||
res.Header.Set("Server", serverName)
|
||||
return res
|
||||
}
|
||||
|
||||
func (s *Session) NewErrorResponse(err error) *http.Response {
|
||||
return NewErrorResponse(s.request, err)
|
||||
}
|
||||
|
||||
func NewSession(conn net.Conn, request *http.Request, response *http.Response) *Session {
|
||||
return &Session{
|
||||
conn: conn,
|
||||
request: request,
|
||||
response: response,
|
||||
props: map[string]any{},
|
||||
}
|
||||
}
|
100
listener/mitm/utils.go
Normal file
100
listener/mitm/utils.go
Normal file
@ -0,0 +1,100 @@
|
||||
package mitm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/encoding/charmap"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
type MultiReaderConn struct {
|
||||
net.Conn
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
func (c *MultiReaderConn) Read(buf []byte) (int, error) {
|
||||
return c.reader.Read(buf)
|
||||
}
|
||||
|
||||
func NewResponse(code int, body io.Reader, req *http.Request) *http.Response {
|
||||
if body == nil {
|
||||
body = &bytes.Buffer{}
|
||||
}
|
||||
|
||||
rc, ok := body.(io.ReadCloser)
|
||||
if !ok {
|
||||
rc = ioutil.NopCloser(body)
|
||||
}
|
||||
|
||||
res := &http.Response{
|
||||
StatusCode: code,
|
||||
Status: fmt.Sprintf("%d %s", code, http.StatusText(code)),
|
||||
Proto: "HTTP/1.1",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: http.Header{},
|
||||
Body: rc,
|
||||
Request: req,
|
||||
}
|
||||
|
||||
if req != nil {
|
||||
res.Close = req.Close
|
||||
res.Proto = req.Proto
|
||||
res.ProtoMajor = req.ProtoMajor
|
||||
res.ProtoMinor = req.ProtoMinor
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func NewErrorResponse(req *http.Request, err error) *http.Response {
|
||||
res := NewResponse(http.StatusBadGateway, nil, req)
|
||||
res.Close = true
|
||||
|
||||
date := res.Header.Get("Date")
|
||||
if date == "" {
|
||||
date = time.Now().Format(http.TimeFormat)
|
||||
}
|
||||
|
||||
w := fmt.Sprintf(`199 "clash" %q %q`, err.Error(), date)
|
||||
res.Header.Add("Warning", w)
|
||||
res.Header.Set("Server", serverName)
|
||||
return res
|
||||
}
|
||||
|
||||
func ReadDecompressedBody(res *http.Response) ([]byte, error) {
|
||||
rBody := res.Body
|
||||
if res.Header.Get("Content-Encoding") == "gzip" {
|
||||
gzReader, err := gzip.NewReader(rBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rBody = gzReader
|
||||
|
||||
defer func(gzReader *gzip.Reader) {
|
||||
_ = gzReader.Close()
|
||||
}(gzReader)
|
||||
}
|
||||
return ioutil.ReadAll(rBody)
|
||||
}
|
||||
|
||||
func DecodeLatin1(reader io.Reader) (string, error) {
|
||||
r := transform.NewReader(reader, charmap.ISO8859_1.NewDecoder())
|
||||
b, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func EncodeLatin1(str string) ([]byte, error) {
|
||||
return charmap.ISO8859_1.NewEncoder().Bytes([]byte(str))
|
||||
}
|
@ -16,7 +16,7 @@ import (
|
||||
type Listener struct {
|
||||
listener net.Listener
|
||||
addr string
|
||||
cache *cache.Cache
|
||||
cache *cache.Cache[string, bool]
|
||||
closed bool
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@ func New(addr string, in chan<- C.ConnContext) (*Listener, error) {
|
||||
ml := &Listener{
|
||||
listener: l,
|
||||
addr: addr,
|
||||
cache: cache.New(30 * time.Second),
|
||||
cache: cache.New[string, bool](30 * time.Second),
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
@ -63,7 +63,7 @@ func New(addr string, in chan<- C.ConnContext) (*Listener, error) {
|
||||
return ml, nil
|
||||
}
|
||||
|
||||
func handleConn(conn net.Conn, in chan<- C.ConnContext, cache *cache.Cache) {
|
||||
func handleConn(conn net.Conn, in chan<- C.ConnContext, cache *cache.Cache[string, bool]) {
|
||||
conn.(*net.TCPConn).SetKeepAlive(true)
|
||||
|
||||
bufConn := N.NewBufferedConn(conn)
|
||||
|
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
|
||||
}
|
@ -39,6 +39,8 @@ func ParseRule(tp, payload, target string, params []string) (C.Rule, error) {
|
||||
parsed, parseErr = NewProcess(payload, target, false)
|
||||
case "SCRIPT":
|
||||
parsed, parseErr = NewScript(payload, target)
|
||||
case "USER-AGENT":
|
||||
parsed, parseErr = NewUserAgent(payload, target)
|
||||
case "MATCH":
|
||||
parsed = NewMatch(target)
|
||||
default:
|
||||
|
52
rule/user_gent.go
Normal file
52
rule/user_gent.go
Normal file
@ -0,0 +1,52 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type UserAgent struct {
|
||||
*Base
|
||||
ua string
|
||||
adapter string
|
||||
}
|
||||
|
||||
func (d *UserAgent) RuleType() C.RuleType {
|
||||
return C.UserAgent
|
||||
}
|
||||
|
||||
func (d *UserAgent) Match(metadata *C.Metadata) bool {
|
||||
if metadata.Type != C.MITM || metadata.UserAgent == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
return strings.Contains(metadata.UserAgent, d.ua)
|
||||
}
|
||||
|
||||
func (d *UserAgent) Adapter() string {
|
||||
return d.adapter
|
||||
}
|
||||
|
||||
func (d *UserAgent) Payload() string {
|
||||
return d.ua
|
||||
}
|
||||
|
||||
func (d *UserAgent) ShouldResolveIP() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func NewUserAgent(ua string, adapter string) (*UserAgent, error) {
|
||||
ua = strings.Trim(ua, "*")
|
||||
if ua == "" {
|
||||
return nil, errPayload
|
||||
}
|
||||
|
||||
return &UserAgent{
|
||||
Base: &Base{},
|
||||
ua: ua,
|
||||
adapter: adapter,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ C.Rule = (*UserAgent)(nil)
|
@ -8,7 +8,7 @@ require (
|
||||
github.com/docker/go-connections v0.4.0
|
||||
github.com/miekg/dns v1.1.47
|
||||
github.com/stretchr/testify v1.7.1
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f
|
||||
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3
|
||||
)
|
||||
|
||||
replace github.com/Dreamacro/clash => ../
|
||||
@ -39,10 +39,10 @@ require (
|
||||
github.com/xtls/go v0.0.0-20210920065950-d4af136d3672 // indirect
|
||||
go.etcd.io/bbolt v1.3.6 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect
|
||||
golang.org/x/mod v0.5.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 // indirect
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect
|
||||
golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab // indirect
|
||||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
|
||||
golang.org/x/tools v0.1.9 // indirect
|
||||
|
16
test/go.sum
16
test/go.sum
@ -913,8 +913,8 @@ golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWP
|
||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38=
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 h1:iU7T1X1J6yxDr0rda54sWGkHgOp5XJrqm79gcNlC2VM=
|
||||
golang.org/x/crypto v0.0.0-20220408190544-5352b0902921/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -950,8 +950,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 h1:LQmS1nU0twXLA96Kt7U9qtHJEbBk3z6Q0V4UXjZkpr4=
|
||||
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -1012,8 +1012,8 @@ golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 h1:EN5+DfgmRMvRUrMGERW2gQl3Vc+Z7ZMnI/xdEpPSf0c=
|
||||
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -1144,8 +1144,8 @@ golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 h1:A9i04dxx7Cribqbs8jf3FQLogkL/CV2YN7hj9KWJCkc=
|
||||
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw=
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
|
@ -148,7 +148,7 @@ func (t *Trojan) PresetXTLSConn(conn net.Conn) (net.Conn, error) {
|
||||
xtlsConn.DirectMode = true
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("failed to use %s, maybe \"security\" is not \"xtls\"", t.option.Flow)
|
||||
return conn, fmt.Errorf("failed to use %s, maybe \"security\" is not \"xtls\"", t.option.Flow)
|
||||
}
|
||||
}
|
||||
|
||||
|
54
tunnel/statistic/sniffing.go
Normal file
54
tunnel/statistic/sniffing.go
Normal file
@ -0,0 +1,54 @@
|
||||
package statistic
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/Dreamacro/clash/common/snifer/tls"
|
||||
"github.com/Dreamacro/clash/component/resolver"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
type sniffing struct {
|
||||
C.Conn
|
||||
|
||||
metadata *C.Metadata
|
||||
totalWrite *atomic.Uint64
|
||||
}
|
||||
|
||||
func (r *sniffing) Read(b []byte) (int, error) {
|
||||
return r.Conn.Read(b)
|
||||
}
|
||||
|
||||
func (r *sniffing) Write(b []byte) (int, error) {
|
||||
if r.totalWrite.Load() < 128 && r.metadata.Host == "" && (r.metadata.DstPort == "443" || r.metadata.DstPort == "8443" || r.metadata.DstPort == "993" || r.metadata.DstPort == "465" || r.metadata.DstPort == "995") {
|
||||
header, err := tls.SniffTLS(b)
|
||||
if err != nil {
|
||||
// log.Errorln("Expect no error but actually %s %s:%s:%s", err.Error(), tt.Metadata.Host, tt.Metadata.DstIP.String(), tt.Metadata.DstPort)
|
||||
} else {
|
||||
resolver.InsertHostByIP(r.metadata.DstIP, header.Domain())
|
||||
log.Warnln("use sni update host: %s ip: %s", header.Domain(), r.metadata.DstIP.String())
|
||||
r.Conn.Close()
|
||||
return 0, errors.New("sni update, break current link to avoid leaks")
|
||||
}
|
||||
}
|
||||
|
||||
n, err := r.Conn.Write(b)
|
||||
r.totalWrite.Add(uint64(n))
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *sniffing) Close() error {
|
||||
return r.Conn.Close()
|
||||
}
|
||||
|
||||
func NewSniffing(conn C.Conn, metadata *C.Metadata) C.Conn {
|
||||
return &sniffing{
|
||||
Conn: conn,
|
||||
metadata: metadata,
|
||||
totalWrite: atomic.NewUint64(0),
|
||||
}
|
||||
}
|
@ -57,7 +57,7 @@ func (tt *tcpTracker) Close() error {
|
||||
return tt.Conn.Close()
|
||||
}
|
||||
|
||||
func NewTCPTracker(conn C.Conn, manager *Manager, metadata *C.Metadata, rule C.Rule) *tcpTracker {
|
||||
func NewTCPTracker(conn C.Conn, manager *Manager, metadata *C.Metadata, rule C.Rule) C.Conn {
|
||||
uuid, _ := uuid.NewV4()
|
||||
|
||||
t := &tcpTracker{
|
||||
@ -80,7 +80,7 @@ func NewTCPTracker(conn C.Conn, manager *Manager, metadata *C.Metadata, rule C.R
|
||||
}
|
||||
|
||||
manager.Join(t)
|
||||
return t
|
||||
return NewSniffing(t, metadata)
|
||||
}
|
||||
|
||||
type udpTracker struct {
|
||||
|
@ -27,6 +27,7 @@ var (
|
||||
udpQueue = make(chan *inbound.PacketAdapter, 200)
|
||||
natTable = nat.New()
|
||||
rules []C.Rule
|
||||
rewrites C.RewriteRule
|
||||
proxies = make(map[string]C.Proxy)
|
||||
providers map[string]provider.ProxyProvider
|
||||
configMux sync.RWMutex
|
||||
@ -36,6 +37,9 @@ var (
|
||||
|
||||
// default timeout for UDP session
|
||||
udpTimeout = 60 * time.Second
|
||||
|
||||
// MitmOutbound mitm proxy adapter
|
||||
MitmOutbound C.ProxyAdapter
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -92,6 +96,18 @@ func SetMode(m TunnelMode) {
|
||||
mode = m
|
||||
}
|
||||
|
||||
// Rewrites return all rewrites
|
||||
func Rewrites() C.RewriteRule {
|
||||
return rewrites
|
||||
}
|
||||
|
||||
// UpdateRewrites handle update rewrites
|
||||
func UpdateRewrites(newRewrites C.RewriteRule) {
|
||||
configMux.Lock()
|
||||
rewrites = newRewrites
|
||||
configMux.Unlock()
|
||||
}
|
||||
|
||||
// processUDP starts a loop to handle udp packet
|
||||
func processUDP() {
|
||||
queue := udpQueue
|
||||
@ -143,7 +159,7 @@ func preHandleMetadata(metadata *C.Metadata) error {
|
||||
metadata.DNSMode = C.DNSFakeIP
|
||||
} else if node := resolver.DefaultHosts.Search(host); node != nil {
|
||||
// redir-host should lookup the hosts
|
||||
metadata.DstIP = node.Data.(net.IP)
|
||||
metadata.DstIP = node.Data.AsSlice()
|
||||
}
|
||||
} else if resolver.IsFakeIP(metadata.DstIP) {
|
||||
return fmt.Errorf("fake DNS record %s missing", metadata.DstIP)
|
||||
@ -286,14 +302,24 @@ func handleTCPConn(connCtx C.ConnContext) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
|
||||
defer cancel()
|
||||
if MitmOutbound != nil && metadata.Type != C.MITM {
|
||||
if remoteConn, err1 := MitmOutbound.DialContext(ctx, metadata); err1 == nil {
|
||||
remoteConn = statistic.NewSniffing(remoteConn, metadata)
|
||||
defer remoteConn.Close()
|
||||
|
||||
handleSocket(connCtx, remoteConn)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
proxy, rule, err := resolveMetadata(connCtx, metadata)
|
||||
if err != nil {
|
||||
log.Warnln("[Metadata] parse failed: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
|
||||
defer cancel()
|
||||
remoteConn, err := proxy.DialContext(ctx, metadata.Pure())
|
||||
if err != nil {
|
||||
if rule == nil {
|
||||
@ -333,8 +359,7 @@ func match(metadata *C.Metadata) (C.Proxy, C.Rule, error) {
|
||||
var resolved bool
|
||||
|
||||
if node := resolver.DefaultHosts.Search(metadata.Host); node != nil {
|
||||
ip := node.Data.(net.IP)
|
||||
metadata.DstIP = ip
|
||||
metadata.DstIP = node.Data.AsSlice()
|
||||
resolved = true
|
||||
}
|
||||
|
||||
@ -388,8 +413,7 @@ func matchScript(metadata *C.Metadata) (C.Proxy, error) {
|
||||
defer configMux.RUnlock()
|
||||
|
||||
if node := resolver.DefaultHosts.Search(metadata.Host); node != nil {
|
||||
ip := node.Data.(net.IP)
|
||||
metadata.DstIP = ip
|
||||
metadata.DstIP = node.Data.AsSlice()
|
||||
}
|
||||
|
||||
adapter, err := S.CallPyMainFunction(metadata)
|
||||
|
Reference in New Issue
Block a user