Chore: merge branch 'with-tun' into plus-pro
This commit is contained in:
commit
05b4a326de
40
README.md
40
README.md
@ -36,12 +36,44 @@
|
|||||||
Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki).
|
Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki).
|
||||||
|
|
||||||
## Advanced usage for this branch
|
## Advanced usage for this branch
|
||||||
|
### MITM configuration
|
||||||
|
A root CA certificate is required, the
|
||||||
|
MITM proxy server will generate a CA certificate file and a CA private key file in your Clash home directory, you can use your own certificate replace it.
|
||||||
|
|
||||||
|
Need to install and trust the CA certificate on the client device, open this URL http://mitm.clash/cert.crt by the web browser to install the CA certificate, the host name 'mitm.clash' was always been hijacked.
|
||||||
|
|
||||||
|
NOTE: this feature cannot work on tls pinning
|
||||||
|
|
||||||
|
WARNING: DO NOT USE THIS FEATURE TO BREAK LOCAL LAWS
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Port of MITM proxy server on the local end
|
||||||
|
mitm-port: 7894
|
||||||
|
|
||||||
|
# Man-In-The-Middle attack
|
||||||
|
mitm:
|
||||||
|
hosts: # use for others proxy type. E.g: TUN, socks
|
||||||
|
- +.example.com
|
||||||
|
rules: # rewrite rules
|
||||||
|
- '^https?://www\.example\.com/1 url reject' # The "reject" returns HTTP status code 404 with no content.
|
||||||
|
- '^https?://www\.example\.com/2 url reject-200' # The "reject-200" returns HTTP status code 200 with no content.
|
||||||
|
- '^https?://www\.example\.com/3 url reject-img' # The "reject-img" returns HTTP status code 200 with content of 1px png.
|
||||||
|
- '^https?://www\.example\.com/4 url reject-dict' # The "reject-dict" returns HTTP status code 200 with content of empty json object.
|
||||||
|
- '^https?://www\.example\.com/5 url reject-array' # The "reject-array" returns HTTP status code 200 with content of empty json array.
|
||||||
|
- '^https?://www\.example\.com/(6) url 302 https://www.example.com/new-$1'
|
||||||
|
- '^https?://www\.(example)\.com/7 url 307 https://www.$1.com/new-7'
|
||||||
|
- '^https?://www\.example\.com/8 url request-header (\r\n)User-Agent:.+(\r\n) request-header $1User-Agent: haha-wriohoh$2' # The "request-header" works for all the http headers not just one single header, so you can match two or more headers including CRLF in one regular expression.
|
||||||
|
- '^https?://www\.example\.com/9 url request-body "pos_2":\[.*\],"pos_3" request-body "pos_2":[{"xx": "xx"}],"pos_3"'
|
||||||
|
- '^https?://www\.example\.com/10 url response-header (\r\n)Tracecode:.+(\r\n) response-header $1Tracecode: 88888888888$2'
|
||||||
|
- '^https?://www\.example\.com/11 url response-body "errmsg":"ok" response-body "errmsg":"not-ok"'
|
||||||
|
```
|
||||||
|
|
||||||
### DNS configuration
|
### DNS configuration
|
||||||
Support resolve ip with a proxy tunnel.
|
Support resolve ip with a proxy tunnel.
|
||||||
|
|
||||||
Support `geosite` with `fallback-filter`.
|
Support `geosite` with `fallback-filter`.
|
||||||
|
|
||||||
Use curl -X POST controllerip:port/cache/fakeip/flush to flush persistence fakeip
|
Use `curl -X POST controllerip:port/cache/fakeip/flush` to flush persistence fakeip
|
||||||
```yaml
|
```yaml
|
||||||
dns:
|
dns:
|
||||||
enable: true
|
enable: true
|
||||||
@ -85,6 +117,7 @@ tun:
|
|||||||
```
|
```
|
||||||
### Rules configuration
|
### Rules configuration
|
||||||
- Support rule `GEOSITE`.
|
- Support rule `GEOSITE`.
|
||||||
|
- Support rule `USER-AGENT`.
|
||||||
- Support `multiport` condition for rule `SRC-PORT` and `DST-PORT`.
|
- Support `multiport` condition for rule `SRC-PORT` and `DST-PORT`.
|
||||||
- Support `network` condition for all rules.
|
- Support `network` condition for all rules.
|
||||||
- Support `process` condition for all rules.
|
- Support `process` condition for all rules.
|
||||||
@ -115,7 +148,10 @@ rules:
|
|||||||
|
|
||||||
# multiport condition for rules SRC-PORT and DST-PORT
|
# multiport condition for rules SRC-PORT and DST-PORT
|
||||||
- DST-PORT,123/136/137-139,DIRECT,udp
|
- DST-PORT,123/136/137-139,DIRECT,udp
|
||||||
|
|
||||||
|
# USER-AGENT payload cannot include the comma character, '*' meaning any character.
|
||||||
|
- USER-AGENT,*example*,PROXY
|
||||||
|
|
||||||
# rule GEOSITE
|
# rule GEOSITE
|
||||||
- GEOSITE,category-ads-all,REJECT
|
- GEOSITE,category-ads-all,REJECT
|
||||||
- GEOSITE,icloud@cn,DIRECT
|
- GEOSITE,icloud@cn,DIRECT
|
||||||
|
@ -3,11 +3,13 @@ package adapter
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
_ "unsafe"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/queue"
|
"github.com/Dreamacro/clash/common/queue"
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
@ -16,9 +18,12 @@ import (
|
|||||||
"go.uber.org/atomic"
|
"go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:linkname errCanceled net.errCanceled
|
||||||
|
var errCanceled error
|
||||||
|
|
||||||
type Proxy struct {
|
type Proxy struct {
|
||||||
C.ProxyAdapter
|
C.ProxyAdapter
|
||||||
history *queue.Queue
|
history *queue.Queue[C.DelayHistory]
|
||||||
alive *atomic.Bool
|
alive *atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,7 +42,7 @@ func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
|
|||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||||
conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...)
|
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
|
return conn, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +65,7 @@ func (p *Proxy) DelayHistory() []C.DelayHistory {
|
|||||||
queue := p.history.Copy()
|
queue := p.history.Copy()
|
||||||
histories := []C.DelayHistory{}
|
histories := []C.DelayHistory{}
|
||||||
for _, item := range queue {
|
for _, item := range queue {
|
||||||
histories = append(histories, item.(C.DelayHistory))
|
histories = append(histories, item)
|
||||||
}
|
}
|
||||||
return histories
|
return histories
|
||||||
}
|
}
|
||||||
@ -73,11 +78,7 @@ func (p *Proxy) LastDelay() (delay uint16) {
|
|||||||
return max
|
return max
|
||||||
}
|
}
|
||||||
|
|
||||||
last := p.history.Last()
|
history := p.history.Last()
|
||||||
if last == nil {
|
|
||||||
return max
|
|
||||||
}
|
|
||||||
history := last.(C.DelayHistory)
|
|
||||||
if history.Delay == 0 {
|
if history.Delay == 0 {
|
||||||
return max
|
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 {
|
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) {
|
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)))
|
req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if metadata.Type == C.MITM {
|
||||||
|
req.Header.Add("Origin-Request-Source-Address", metadata.SourceAddress())
|
||||||
|
}
|
||||||
|
|
||||||
if err := req.Write(rw); err != nil {
|
if err := req.Write(rw); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
68
adapter/outbound/mitm.go
Normal file
68
adapter/outbound/mitm.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
|
"github.com/Dreamacro/clash/component/trie"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
|
||||||
|
"go.uber.org/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errIgnored = errors.New("not match in mitm host lists")
|
||||||
|
httpProxyClient = NewHttp(HttpOption{})
|
||||||
|
|
||||||
|
MiddlemanServerAddress = atomic.NewString("")
|
||||||
|
MiddlemanRewriteHosts *trie.DomainTrie[bool]
|
||||||
|
)
|
||||||
|
|
||||||
|
type Mitm struct {
|
||||||
|
*Base
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialContext implements C.ProxyAdapter
|
||||||
|
func (d *Mitm) DialContext(ctx context.Context, metadata *C.Metadata, _ ...dialer.Option) (C.Conn, error) {
|
||||||
|
addr := MiddlemanServerAddress.Load()
|
||||||
|
if addr == "" || MiddlemanRewriteHosts == nil {
|
||||||
|
return nil, errIgnored
|
||||||
|
}
|
||||||
|
|
||||||
|
if MiddlemanRewriteHosts.Search(metadata.String()) == nil {
|
||||||
|
return nil, errIgnored
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata.Type = C.MITM
|
||||||
|
|
||||||
|
if metadata.Host != "" {
|
||||||
|
metadata.AddrType = C.AtypDomainName
|
||||||
|
metadata.DstIP = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := dialer.DialContext(ctx, "tcp", addr, []dialer.Option{dialer.WithInterface(""), dialer.WithRoutingMark(0)}...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tcpKeepAlive(c)
|
||||||
|
|
||||||
|
defer safeConnClose(c, err)
|
||||||
|
|
||||||
|
c, err = httpProxyClient.StreamConn(c, metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewConn(c, d), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMitm() *Mitm {
|
||||||
|
return &Mitm{
|
||||||
|
Base: &Base{
|
||||||
|
name: "Mitm",
|
||||||
|
tp: C.Mitm,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
48
common/cache/cache.go
vendored
48
common/cache/cache.go
vendored
@ -7,50 +7,50 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Cache store element with a expired time
|
// Cache store element with a expired time
|
||||||
type Cache struct {
|
type Cache[K comparable, V any] struct {
|
||||||
*cache
|
*cache[K, V]
|
||||||
}
|
}
|
||||||
|
|
||||||
type cache struct {
|
type cache[K comparable, V any] struct {
|
||||||
mapping sync.Map
|
mapping sync.Map
|
||||||
janitor *janitor
|
janitor *janitor[K, V]
|
||||||
}
|
}
|
||||||
|
|
||||||
type element struct {
|
type element[V any] struct {
|
||||||
Expired time.Time
|
Expired time.Time
|
||||||
Payload any
|
Payload V
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put element in Cache with its ttl
|
// Put element in Cache with its ttl
|
||||||
func (c *cache) Put(key any, payload any, ttl time.Duration) {
|
func (c *cache[K, V]) Put(key K, payload V, ttl time.Duration) {
|
||||||
c.mapping.Store(key, &element{
|
c.mapping.Store(key, &element[V]{
|
||||||
Payload: payload,
|
Payload: payload,
|
||||||
Expired: time.Now().Add(ttl),
|
Expired: time.Now().Add(ttl),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get element in Cache, and drop when it expired
|
// 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)
|
item, exist := c.mapping.Load(key)
|
||||||
if !exist {
|
if !exist {
|
||||||
return nil
|
return getZero[V]()
|
||||||
}
|
}
|
||||||
elm := item.(*element)
|
elm := item.(*element[V])
|
||||||
// expired
|
// expired
|
||||||
if time.Since(elm.Expired) > 0 {
|
if time.Since(elm.Expired) > 0 {
|
||||||
c.mapping.Delete(key)
|
c.mapping.Delete(key)
|
||||||
return nil
|
return getZero[V]()
|
||||||
}
|
}
|
||||||
return elm.Payload
|
return elm.Payload
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWithExpire element in Cache with Expire Time
|
// 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)
|
item, exist := c.mapping.Load(key)
|
||||||
if !exist {
|
if !exist {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
elm := item.(*element)
|
elm := item.(*element[V])
|
||||||
// expired
|
// expired
|
||||||
if time.Since(elm.Expired) > 0 {
|
if time.Since(elm.Expired) > 0 {
|
||||||
c.mapping.Delete(key)
|
c.mapping.Delete(key)
|
||||||
@ -59,10 +59,10 @@ func (c *cache) GetWithExpire(key any) (payload any, expired time.Time) {
|
|||||||
return elm.Payload, elm.Expired
|
return elm.Payload, elm.Expired
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cache) cleanup() {
|
func (c *cache[K, V]) cleanup() {
|
||||||
c.mapping.Range(func(k, v any) bool {
|
c.mapping.Range(func(k, v any) bool {
|
||||||
key := k.(string)
|
key := k.(string)
|
||||||
elm := v.(*element)
|
elm := v.(*element[V])
|
||||||
if time.Since(elm.Expired) > 0 {
|
if time.Since(elm.Expired) > 0 {
|
||||||
c.mapping.Delete(key)
|
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
|
interval time.Duration
|
||||||
stop chan struct{}
|
stop chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *janitor) process(c *cache) {
|
func (j *janitor[K, V]) process(c *cache[K, V]) {
|
||||||
ticker := time.NewTicker(j.interval)
|
ticker := time.NewTicker(j.interval)
|
||||||
for {
|
for {
|
||||||
select {
|
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{}{}
|
c.janitor.stop <- struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New return *Cache
|
// New return *Cache
|
||||||
func New(interval time.Duration) *Cache {
|
func New[K comparable, V any](interval time.Duration) *Cache[K, V] {
|
||||||
j := &janitor{
|
j := &janitor[K, V]{
|
||||||
interval: interval,
|
interval: interval,
|
||||||
stop: make(chan struct{}),
|
stop: make(chan struct{}),
|
||||||
}
|
}
|
||||||
c := &cache{janitor: j}
|
c := &cache[K, V]{janitor: j}
|
||||||
go j.process(c)
|
go j.process(c)
|
||||||
C := &Cache{c}
|
C := &Cache[K, V]{c}
|
||||||
runtime.SetFinalizer(C, stopJanitor)
|
runtime.SetFinalizer(C, stopJanitor[K, V])
|
||||||
return C
|
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) {
|
func TestCache_Basic(t *testing.T) {
|
||||||
interval := 200 * time.Millisecond
|
interval := 200 * time.Millisecond
|
||||||
ttl := 20 * time.Millisecond
|
ttl := 20 * time.Millisecond
|
||||||
c := New(interval)
|
c := New[string, int](interval)
|
||||||
c.Put("int", 1, ttl)
|
c.Put("int", 1, ttl)
|
||||||
c.Put("string", "a", ttl)
|
|
||||||
|
d := New[string, string](interval)
|
||||||
|
d.Put("string", "a", ttl)
|
||||||
|
|
||||||
i := c.Get("int")
|
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")
|
s := d.Get("string")
|
||||||
assert.Equal(t, s.(string), "a", "should recv 'a'")
|
assert.Equal(t, s, "a", "should recv 'a'")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCache_TTL(t *testing.T) {
|
func TestCache_TTL(t *testing.T) {
|
||||||
interval := 200 * time.Millisecond
|
interval := 200 * time.Millisecond
|
||||||
ttl := 20 * time.Millisecond
|
ttl := 20 * time.Millisecond
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
c := New(interval)
|
c := New[string, int](interval)
|
||||||
c.Put("int", 1, ttl)
|
c.Put("int", 1, ttl)
|
||||||
c.Put("int2", 2, ttl)
|
c.Put("int2", 2, ttl)
|
||||||
|
|
||||||
i := c.Get("int")
|
i := c.Get("int")
|
||||||
_, expired := c.GetWithExpire("int2")
|
_, 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))
|
assert.True(t, now.Before(expired))
|
||||||
|
|
||||||
time.Sleep(ttl * 2)
|
time.Sleep(ttl * 2)
|
||||||
i = c.Get("int")
|
i = c.Get("int")
|
||||||
j, _ := c.GetWithExpire("int2")
|
j, _ := c.GetWithExpire("int2")
|
||||||
assert.Nil(t, i, "should recv nil")
|
assert.True(t, i == 0, "should recv 0")
|
||||||
assert.Nil(t, j, "should recv nil")
|
assert.True(t, j == 0, "should recv 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCache_AutoCleanup(t *testing.T) {
|
func TestCache_AutoCleanup(t *testing.T) {
|
||||||
interval := 10 * time.Millisecond
|
interval := 10 * time.Millisecond
|
||||||
ttl := 15 * time.Millisecond
|
ttl := 15 * time.Millisecond
|
||||||
c := New(interval)
|
c := New[string, int](interval)
|
||||||
c.Put("int", 1, ttl)
|
c.Put("int", 1, ttl)
|
||||||
|
|
||||||
time.Sleep(ttl * 2)
|
time.Sleep(ttl * 2)
|
||||||
i := c.Get("int")
|
i := c.Get("int")
|
||||||
j, _ := c.GetWithExpire("int")
|
j, _ := c.GetWithExpire("int")
|
||||||
assert.Nil(t, i, "should recv nil")
|
assert.True(t, i == 0, "should recv 0")
|
||||||
assert.Nil(t, j, "should recv nil")
|
assert.True(t, j == 0, "should recv 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCache_AutoGC(t *testing.T) {
|
func TestCache_AutoGC(t *testing.T) {
|
||||||
@ -60,7 +62,7 @@ func TestCache_AutoGC(t *testing.T) {
|
|||||||
go func() {
|
go func() {
|
||||||
interval := 10 * time.Millisecond
|
interval := 10 * time.Millisecond
|
||||||
ttl := 15 * time.Millisecond
|
ttl := 15 * time.Millisecond
|
||||||
c := New(interval)
|
c := New[string, int](interval)
|
||||||
c.Put("int", 1, ttl)
|
c.Put("int", 1, ttl)
|
||||||
sign <- struct{}{}
|
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
|
// 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
|
// EvictCallback is used to get a callback when a cache entry is evicted
|
||||||
type EvictCallback = func(key any, value any)
|
type EvictCallback = func(key any, value any)
|
||||||
|
|
||||||
// WithEvict set the evict callback
|
// WithEvict set the evict callback
|
||||||
func WithEvict(cb EvictCallback) Option {
|
func WithEvict[K comparable, V any](cb EvictCallback) Option[K, V] {
|
||||||
return func(l *LruCache) {
|
return func(l *LruCache[K, V]) {
|
||||||
l.onEvict = cb
|
l.onEvict = cb
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithUpdateAgeOnGet update expires when Get element
|
// WithUpdateAgeOnGet update expires when Get element
|
||||||
func WithUpdateAgeOnGet() Option {
|
func WithUpdateAgeOnGet[K comparable, V any]() Option[K, V] {
|
||||||
return func(l *LruCache) {
|
return func(l *LruCache[K, V]) {
|
||||||
l.updateAgeOnGet = true
|
l.updateAgeOnGet = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithAge defined element max age (second)
|
// WithAge defined element max age (second)
|
||||||
func WithAge(maxAge int64) Option {
|
func WithAge[K comparable, V any](maxAge int64) Option[K, V] {
|
||||||
return func(l *LruCache) {
|
return func(l *LruCache[K, V]) {
|
||||||
l.maxAge = maxAge
|
l.maxAge = maxAge
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithSize defined max length of LruCache
|
// WithSize defined max length of LruCache
|
||||||
func WithSize(maxSize int) Option {
|
func WithSize[K comparable, V any](maxSize int) Option[K, V] {
|
||||||
return func(l *LruCache) {
|
return func(l *LruCache[K, V]) {
|
||||||
l.maxSize = maxSize
|
l.maxSize = maxSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithStale decide whether Stale return is enabled.
|
// WithStale decide whether Stale return is enabled.
|
||||||
// If this feature is enabled, element will not get Evicted according to `WithAge`.
|
// If this feature is enabled, element will not get Evicted according to `WithAge`.
|
||||||
func WithStale(stale bool) Option {
|
func WithStale[K comparable, V any](stale bool) Option[K, V] {
|
||||||
return func(l *LruCache) {
|
return func(l *LruCache[K, V]) {
|
||||||
l.staleReturn = stale
|
l.staleReturn = stale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -53,7 +53,7 @@ func WithStale(stale bool) Option {
|
|||||||
// LruCache is a thread-safe, in-memory lru-cache that evicts the
|
// LruCache is a thread-safe, in-memory lru-cache that evicts the
|
||||||
// least recently used entries from memory when (if set) the entries are
|
// least recently used entries from memory when (if set) the entries are
|
||||||
// older than maxAge (in seconds). Use the New constructor to create one.
|
// older than maxAge (in seconds). Use the New constructor to create one.
|
||||||
type LruCache struct {
|
type LruCache[K comparable, V any] struct {
|
||||||
maxAge int64
|
maxAge int64
|
||||||
maxSize int
|
maxSize int
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
@ -65,8 +65,8 @@ type LruCache struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewLRUCache creates an LruCache
|
// NewLRUCache creates an LruCache
|
||||||
func NewLRUCache(options ...Option) *LruCache {
|
func NewLRUCache[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] {
|
||||||
lc := &LruCache{
|
lc := &LruCache[K, V]{
|
||||||
lru: list.New(),
|
lru: list.New(),
|
||||||
cache: make(map[any]*list.Element),
|
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
|
// Get returns the any representation of a cached response and a bool
|
||||||
// set to true if the key was found.
|
// set to true if the key was found.
|
||||||
func (c *LruCache) Get(key any) (any, bool) {
|
func (c *LruCache[K, V]) Get(key K) (V, bool) {
|
||||||
entry := c.get(key)
|
el := c.get(key)
|
||||||
if entry == nil {
|
if el == nil {
|
||||||
return nil, false
|
return getZero[V](), false
|
||||||
}
|
}
|
||||||
value := entry.value
|
value := el.value
|
||||||
|
|
||||||
return value, true
|
return value, true
|
||||||
}
|
}
|
||||||
@ -94,17 +94,17 @@ func (c *LruCache) Get(key any) (any, bool) {
|
|||||||
// a time.Time Give expected expires,
|
// a time.Time Give expected expires,
|
||||||
// and a bool set to true if the key was found.
|
// 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.
|
// 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) {
|
func (c *LruCache[K, V]) GetWithExpire(key K) (V, time.Time, bool) {
|
||||||
entry := c.get(key)
|
el := c.get(key)
|
||||||
if entry == nil {
|
if el == nil {
|
||||||
return nil, time.Time{}, false
|
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
|
// 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()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
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.
|
// 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)
|
expires := int64(0)
|
||||||
if c.maxAge > 0 {
|
if c.maxAge > 0 {
|
||||||
expires = time.Now().Unix() + c.maxAge
|
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.
|
// SetWithExpire stores the any representation of a response for a given key and given expires.
|
||||||
// The expires time will round to second.
|
// 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()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
if le, ok := c.cache[key]; ok {
|
if le, ok := c.cache[key]; ok {
|
||||||
c.lru.MoveToBack(le)
|
c.lru.MoveToBack(le)
|
||||||
e := le.Value.(*entry)
|
e := le.Value.(*entry[K, V])
|
||||||
e.value = value
|
e.value = value
|
||||||
e.expires = expires.Unix()
|
e.expires = expires.Unix()
|
||||||
} else {
|
} 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)
|
c.cache[key] = c.lru.PushBack(e)
|
||||||
|
|
||||||
if c.maxSize > 0 {
|
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())
|
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
|
// 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()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
@ -158,12 +158,12 @@ func (c *LruCache) CloneTo(n *LruCache) {
|
|||||||
n.cache = make(map[any]*list.Element)
|
n.cache = make(map[any]*list.Element)
|
||||||
|
|
||||||
for e := c.lru.Front(); e != nil; e = e.Next() {
|
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)
|
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()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
@ -172,7 +172,7 @@ func (c *LruCache) get(key any) *entry {
|
|||||||
return nil
|
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.deleteElement(le)
|
||||||
c.maybeDeleteOldest()
|
c.maybeDeleteOldest()
|
||||||
|
|
||||||
@ -180,15 +180,15 @@ func (c *LruCache) get(key any) *entry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.lru.MoveToBack(le)
|
c.lru.MoveToBack(le)
|
||||||
entry := le.Value.(*entry)
|
el := le.Value.(*entry[K, V])
|
||||||
if c.maxAge > 0 && c.updateAgeOnGet {
|
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.
|
// 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()
|
c.mu.Lock()
|
||||||
|
|
||||||
if le, ok := c.cache[key]; ok {
|
if le, ok := c.cache[key]; ok {
|
||||||
@ -198,25 +198,25 @@ func (c *LruCache) Delete(key any) {
|
|||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *LruCache) maybeDeleteOldest() {
|
func (c *LruCache[K, V]) maybeDeleteOldest() {
|
||||||
if !c.staleReturn && c.maxAge > 0 {
|
if !c.staleReturn && c.maxAge > 0 {
|
||||||
now := time.Now().Unix()
|
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)
|
c.deleteElement(le)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *LruCache) deleteElement(le *list.Element) {
|
func (c *LruCache[K, V]) deleteElement(le *list.Element) {
|
||||||
c.lru.Remove(le)
|
c.lru.Remove(le)
|
||||||
e := le.Value.(*entry)
|
e := le.Value.(*entry[K, V])
|
||||||
delete(c.cache, e.key)
|
delete(c.cache, e.key)
|
||||||
if c.onEvict != nil {
|
if c.onEvict != nil {
|
||||||
c.onEvict(e.key, e.value)
|
c.onEvict(e.key, e.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *LruCache) Clear() error {
|
func (c *LruCache[K, V]) Clear() error {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
|
|
||||||
c.cache = make(map[any]*list.Element)
|
c.cache = make(map[any]*list.Element)
|
||||||
@ -225,8 +225,13 @@ func (c *LruCache) Clear() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type entry struct {
|
type entry[K comparable, V any] struct {
|
||||||
key any
|
key K
|
||||||
value any
|
value V
|
||||||
expires int64
|
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) {
|
func TestLRUCache(t *testing.T) {
|
||||||
c := NewLRUCache()
|
c := NewLRUCache[string, string]()
|
||||||
|
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
c.Set(e.key, e.value)
|
c.Set(e.key, e.value)
|
||||||
@ -32,7 +32,7 @@ func TestLRUCache(t *testing.T) {
|
|||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
value, ok := c.Get(e.key)
|
value, ok := c.Get(e.key)
|
||||||
if assert.True(t, ok) {
|
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) {
|
func TestLRUMaxAge(t *testing.T) {
|
||||||
c := NewLRUCache(WithAge(86400))
|
c := NewLRUCache[string, string](WithAge[string, string](86400))
|
||||||
|
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
expected := now + 86400
|
expected := now + 86400
|
||||||
|
|
||||||
// Add one expired entry
|
// Add one expired entry
|
||||||
c.Set("foo", "bar")
|
c.Set("foo", "bar")
|
||||||
c.lru.Back().Value.(*entry).expires = now
|
c.lru.Back().Value.(*entry[string, string]).expires = now
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
c.Set("foo", "bar")
|
c.Set("foo", "bar")
|
||||||
e := c.lru.Back().Value.(*entry)
|
e := c.lru.Back().Value.(*entry[string, string])
|
||||||
assert.True(t, e.expires >= now)
|
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
|
// Set a few and verify expiration times
|
||||||
for _, s := range entries {
|
for _, s := range entries {
|
||||||
c.Set(s.key, s.value)
|
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)
|
assert.True(t, e.expires >= expected && e.expires <= expected+10)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ func TestLRUMaxAge(t *testing.T) {
|
|||||||
for _, s := range entries {
|
for _, s := range entries {
|
||||||
le, ok := c.cache[s.key]
|
le, ok := c.cache[s.key]
|
||||||
if assert.True(t, ok) {
|
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) {
|
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()
|
now := time.Now().Unix()
|
||||||
expires := now + 86400/2
|
expires := now + 86400/2
|
||||||
|
|
||||||
// Add one expired entry
|
// Add one expired entry
|
||||||
c.Set("foo", "bar")
|
c.Set("foo", "bar")
|
||||||
c.lru.Back().Value.(*entry).expires = expires
|
c.lru.Back().Value.(*entry[string, string]).expires = expires
|
||||||
|
|
||||||
_, ok := c.Get("foo")
|
_, ok := c.Get("foo")
|
||||||
assert.True(t, ok)
|
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) {
|
func TestMaxSize(t *testing.T) {
|
||||||
c := NewLRUCache(WithSize(2))
|
c := NewLRUCache[string, string](WithSize[string, string](2))
|
||||||
// Add one expired entry
|
// Add one expired entry
|
||||||
c.Set("foo", "bar")
|
c.Set("foo", "bar")
|
||||||
_, ok := c.Get("foo")
|
_, ok := c.Get("foo")
|
||||||
@ -117,7 +117,7 @@ func TestMaxSize(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestExist(t *testing.T) {
|
func TestExist(t *testing.T) {
|
||||||
c := NewLRUCache(WithSize(1))
|
c := NewLRUCache[int, int](WithSize[int, int](1))
|
||||||
c.Set(1, 2)
|
c.Set(1, 2)
|
||||||
assert.True(t, c.Exist(1))
|
assert.True(t, c.Exist(1))
|
||||||
c.Set(2, 3)
|
c.Set(2, 3)
|
||||||
@ -130,7 +130,7 @@ func TestEvict(t *testing.T) {
|
|||||||
temp = key.(int) + value.(int)
|
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(1, 2)
|
||||||
c.Set(2, 3)
|
c.Set(2, 3)
|
||||||
|
|
||||||
@ -138,21 +138,22 @@ func TestEvict(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSetWithExpire(t *testing.T) {
|
func TestSetWithExpire(t *testing.T) {
|
||||||
c := NewLRUCache(WithAge(1))
|
c := NewLRUCache[int, *struct{}](WithAge[int, *struct{}](1))
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
|
|
||||||
tenSecBefore := time.Unix(now-10, 0)
|
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 is expected not to exist, and expires should be empty time.Time
|
||||||
res, expires, exist := c.GetWithExpire(1)
|
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, time.Time{}, expires)
|
||||||
assert.Equal(t, false, exist)
|
assert.Equal(t, false, exist)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStale(t *testing.T) {
|
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()
|
now := time.Now().Unix()
|
||||||
|
|
||||||
tenSecBefore := time.Unix(now-10, 0)
|
tenSecBefore := time.Unix(now-10, 0)
|
||||||
@ -165,11 +166,11 @@ func TestStale(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCloneTo(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("1", 1)
|
||||||
o.Set("2", 2)
|
o.Set("2", 2)
|
||||||
|
|
||||||
n := NewLRUCache(WithSize(2))
|
n := NewLRUCache[string, int](WithSize[string, int](2))
|
||||||
n.Set("3", 3)
|
n.Set("3", 3)
|
||||||
n.Set("4", 4)
|
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
|
// Queue is a simple concurrent safe queue
|
||||||
type Queue struct {
|
type Queue[T any] struct {
|
||||||
items []any
|
items []T
|
||||||
lock sync.RWMutex
|
lock sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put add the item to the queue.
|
// Put add the item to the queue.
|
||||||
func (q *Queue) Put(items ...any) {
|
func (q *Queue[T]) Put(items ...T) {
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -22,9 +22,9 @@ func (q *Queue) Put(items ...any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pop returns the head of items.
|
// Pop returns the head of items.
|
||||||
func (q *Queue) Pop() any {
|
func (q *Queue[T]) Pop() T {
|
||||||
if len(q.items) == 0 {
|
if len(q.items) == 0 {
|
||||||
return nil
|
return GetZero[T]()
|
||||||
}
|
}
|
||||||
|
|
||||||
q.lock.Lock()
|
q.lock.Lock()
|
||||||
@ -35,9 +35,9 @@ func (q *Queue) Pop() any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Last returns the last of item.
|
// Last returns the last of item.
|
||||||
func (q *Queue) Last() any {
|
func (q *Queue[T]) Last() T {
|
||||||
if len(q.items) == 0 {
|
if len(q.items) == 0 {
|
||||||
return nil
|
return GetZero[T]()
|
||||||
}
|
}
|
||||||
|
|
||||||
q.lock.RLock()
|
q.lock.RLock()
|
||||||
@ -47,8 +47,8 @@ func (q *Queue) Last() any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Copy get the copy of queue.
|
// Copy get the copy of queue.
|
||||||
func (q *Queue) Copy() []any {
|
func (q *Queue[T]) Copy() []T {
|
||||||
items := []any{}
|
items := []T{}
|
||||||
q.lock.RLock()
|
q.lock.RLock()
|
||||||
items = append(items, q.items...)
|
items = append(items, q.items...)
|
||||||
q.lock.RUnlock()
|
q.lock.RUnlock()
|
||||||
@ -56,7 +56,7 @@ func (q *Queue) Copy() []any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Len returns the number of items in this queue.
|
// Len returns the number of items in this queue.
|
||||||
func (q *Queue) Len() int64 {
|
func (q *Queue[T]) Len() int64 {
|
||||||
q.lock.Lock()
|
q.lock.Lock()
|
||||||
defer q.lock.Unlock()
|
defer q.lock.Unlock()
|
||||||
|
|
||||||
@ -64,8 +64,13 @@ func (q *Queue) Len() int64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New is a constructor for a new concurrent safe queue.
|
// New is a constructor for a new concurrent safe queue.
|
||||||
func New(hint int64) *Queue {
|
func New[T any](hint int64) *Queue[T] {
|
||||||
return &Queue{
|
return &Queue[T]{
|
||||||
items: make([]any, 0, hint),
|
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 {
|
type memoryStore struct {
|
||||||
cache *cache.LruCache
|
cacheIP *cache.LruCache[string, net.IP]
|
||||||
|
cacheHost *cache.LruCache[uint32, string]
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetByHost implements store.GetByHost
|
// GetByHost implements store.GetByHost
|
||||||
func (m *memoryStore) GetByHost(host string) (net.IP, bool) {
|
func (m *memoryStore) GetByHost(host string) (net.IP, bool) {
|
||||||
if elm, exist := m.cache.Get(host); exist {
|
if ip, exist := m.cacheIP.Get(host); exist {
|
||||||
ip := elm.(net.IP)
|
|
||||||
|
|
||||||
// ensure ip --> host on head of linked list
|
// ensure ip --> host on head of linked list
|
||||||
m.cache.Get(ipToUint(ip.To4()))
|
m.cacheHost.Get(ipToUint(ip.To4()))
|
||||||
return ip, true
|
return ip, true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,16 +24,14 @@ func (m *memoryStore) GetByHost(host string) (net.IP, bool) {
|
|||||||
|
|
||||||
// PutByHost implements store.PutByHost
|
// PutByHost implements store.PutByHost
|
||||||
func (m *memoryStore) PutByHost(host string, ip net.IP) {
|
func (m *memoryStore) PutByHost(host string, ip net.IP) {
|
||||||
m.cache.Set(host, ip)
|
m.cacheIP.Set(host, ip)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetByIP implements store.GetByIP
|
// GetByIP implements store.GetByIP
|
||||||
func (m *memoryStore) GetByIP(ip net.IP) (string, bool) {
|
func (m *memoryStore) GetByIP(ip net.IP) (string, bool) {
|
||||||
if elm, exist := m.cache.Get(ipToUint(ip.To4())); exist {
|
if host, exist := m.cacheHost.Get(ipToUint(ip.To4())); exist {
|
||||||
host := elm.(string)
|
|
||||||
|
|
||||||
// ensure host --> ip on head of linked list
|
// ensure host --> ip on head of linked list
|
||||||
m.cache.Get(host)
|
m.cacheIP.Get(host)
|
||||||
return host, true
|
return host, true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,32 +40,41 @@ func (m *memoryStore) GetByIP(ip net.IP) (string, bool) {
|
|||||||
|
|
||||||
// PutByIP implements store.PutByIP
|
// PutByIP implements store.PutByIP
|
||||||
func (m *memoryStore) PutByIP(ip net.IP, host string) {
|
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
|
// DelByIP implements store.DelByIP
|
||||||
func (m *memoryStore) DelByIP(ip net.IP) {
|
func (m *memoryStore) DelByIP(ip net.IP) {
|
||||||
ipNum := ipToUint(ip.To4())
|
ipNum := ipToUint(ip.To4())
|
||||||
if elm, exist := m.cache.Get(ipNum); exist {
|
if host, exist := m.cacheHost.Get(ipNum); exist {
|
||||||
m.cache.Delete(elm.(string))
|
m.cacheIP.Delete(host)
|
||||||
}
|
}
|
||||||
m.cache.Delete(ipNum)
|
m.cacheHost.Delete(ipNum)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exist implements store.Exist
|
// Exist implements store.Exist
|
||||||
func (m *memoryStore) Exist(ip net.IP) bool {
|
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
|
// CloneTo implements store.CloneTo
|
||||||
// only for memoryStore to memoryStore
|
// only for memoryStore to memoryStore
|
||||||
func (m *memoryStore) CloneTo(store store) {
|
func (m *memoryStore) CloneTo(store store) {
|
||||||
if ms, ok := store.(*memoryStore); ok {
|
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
|
// FlushFakeIP implements store.FlushFakeIP
|
||||||
func (m *memoryStore) FlushFakeIP() error {
|
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"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/cache"
|
|
||||||
"github.com/Dreamacro/clash/component/profile/cachefile"
|
"github.com/Dreamacro/clash/component/profile/cachefile"
|
||||||
"github.com/Dreamacro/clash/component/trie"
|
"github.com/Dreamacro/clash/component/trie"
|
||||||
)
|
)
|
||||||
@ -29,7 +28,7 @@ type Pool struct {
|
|||||||
broadcast uint32
|
broadcast uint32
|
||||||
offset uint32
|
offset uint32
|
||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
host *trie.DomainTrie
|
host *trie.DomainTrie[bool]
|
||||||
ipnet *net.IPNet
|
ipnet *net.IPNet
|
||||||
store store
|
store store
|
||||||
}
|
}
|
||||||
@ -139,7 +138,7 @@ func uintToIP(v uint32) net.IP {
|
|||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
IPNet *net.IPNet
|
IPNet *net.IPNet
|
||||||
Host *trie.DomainTrie
|
Host *trie.DomainTrie[bool]
|
||||||
|
|
||||||
// Size sets the maximum number of entries in memory
|
// Size sets the maximum number of entries in memory
|
||||||
// and does not work if Persistence is true
|
// and does not work if Persistence is true
|
||||||
@ -175,9 +174,7 @@ func New(options Options) (*Pool, error) {
|
|||||||
cache: cachefile.Cache(),
|
cache: cachefile.Cache(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
pool.store = &memoryStore{
|
pool.store = newMemoryStore(options.Size)
|
||||||
cache: cache.NewLRUCache(cache.WithSize(options.Size * 2)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return pool, nil
|
return pool, nil
|
||||||
|
@ -100,8 +100,8 @@ func TestPool_CycleUsed(t *testing.T) {
|
|||||||
|
|
||||||
func TestPool_Skip(t *testing.T) {
|
func TestPool_Skip(t *testing.T) {
|
||||||
_, ipnet, _ := net.ParseCIDR("192.168.0.1/29")
|
_, ipnet, _ := net.ParseCIDR("192.168.0.1/29")
|
||||||
tree := trie.New()
|
tree := trie.New[bool]()
|
||||||
tree.Insert("example.com", tree)
|
tree.Insert("example.com", true)
|
||||||
pools, tempfile, err := createPools(Options{
|
pools, tempfile, err := createPools(Options{
|
||||||
IPNet: ipnet,
|
IPNet: ipnet,
|
||||||
Size: 10,
|
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) {
|
func (g GeoIPCache) Unmarshal(filename, code string) (*router.GeoIP, error) {
|
||||||
asset := C.Path.GetAssetLocation(filename)
|
asset := C.Path.Resolve(filename)
|
||||||
idx := strings.ToLower(asset + ":" + code)
|
idx := strings.ToLower(asset + ":" + code)
|
||||||
if g.Has(idx) {
|
if g.Has(idx) {
|
||||||
return g.Get(idx), nil
|
return g.Get(idx), nil
|
||||||
@ -98,7 +98,7 @@ func (g GeoSiteCache) Set(key string, value *router.GeoSite) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g GeoSiteCache) Unmarshal(filename, code string) (*router.GeoSite, error) {
|
func (g GeoSiteCache) Unmarshal(filename, code string) (*router.GeoSite, error) {
|
||||||
asset := C.Path.GetAssetLocation(filename)
|
asset := C.Path.Resolve(filename)
|
||||||
idx := strings.ToLower(asset + ":" + code)
|
idx := strings.ToLower(asset + ":" + code)
|
||||||
if g.Has(idx) {
|
if g.Has(idx) {
|
||||||
return g.Get(idx), nil
|
return g.Get(idx), nil
|
||||||
|
@ -26,7 +26,7 @@ func ReadFile(path string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ReadAsset(file string) ([]byte, error) {
|
func ReadAsset(file string) ([]byte, error) {
|
||||||
return ReadFile(C.Path.GetAssetLocation(file))
|
return ReadFile(C.Path.Resolve(file))
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadIP(filename, country string) ([]*router.CIDR, error) {
|
func loadIP(filename, country string) ([]*router.CIDR, error) {
|
||||||
|
@ -14,6 +14,7 @@ type Enhancer interface {
|
|||||||
IsExistFakeIP(net.IP) bool
|
IsExistFakeIP(net.IP) bool
|
||||||
FindHostByIP(net.IP) (string, bool)
|
FindHostByIP(net.IP) (string, bool)
|
||||||
FlushFakeIP() error
|
FlushFakeIP() error
|
||||||
|
InsertHostByIP(net.IP, string)
|
||||||
}
|
}
|
||||||
|
|
||||||
func FakeIPEnabled() bool {
|
func FakeIPEnabled() bool {
|
||||||
@ -56,6 +57,12 @@ func IsExistFakeIP(ip net.IP) bool {
|
|||||||
return false
|
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) {
|
func FindHostByIP(ip net.IP) (string, bool) {
|
||||||
if mapper := DefaultHostMapper; mapper != nil {
|
if mapper := DefaultHostMapper; mapper != nil {
|
||||||
return mapper.FindHostByIP(ip)
|
return mapper.FindHostByIP(ip)
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -23,7 +24,7 @@ var (
|
|||||||
DisableIPv6 = true
|
DisableIPv6 = true
|
||||||
|
|
||||||
// DefaultHosts aim to resolve hosts
|
// DefaultHosts aim to resolve hosts
|
||||||
DefaultHosts = trie.New()
|
DefaultHosts = trie.New[netip.Addr]()
|
||||||
|
|
||||||
// DefaultDNSTimeout defined the default dns request timeout
|
// DefaultDNSTimeout defined the default dns request timeout
|
||||||
DefaultDNSTimeout = time.Second * 5
|
DefaultDNSTimeout = time.Second * 5
|
||||||
@ -48,8 +49,8 @@ func ResolveIPv4(host string) (net.IP, error) {
|
|||||||
|
|
||||||
func ResolveIPv4WithResolver(host string, r Resolver) (net.IP, error) {
|
func ResolveIPv4WithResolver(host string, r Resolver) (net.IP, error) {
|
||||||
if node := DefaultHosts.Search(host); node != nil {
|
if node := DefaultHosts.Search(host); node != nil {
|
||||||
if ip := node.Data.(net.IP).To4(); ip != nil {
|
if ip := node.Data; ip.Is4() {
|
||||||
return ip, nil
|
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 node := DefaultHosts.Search(host); node != nil {
|
||||||
if ip := node.Data.(net.IP).To16(); ip != nil {
|
if ip := node.Data; ip.Is6() {
|
||||||
return ip, nil
|
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
|
// ResolveIPWithResolver same as ResolveIP, but with a resolver
|
||||||
func ResolveIPWithResolver(host string, r Resolver) (net.IP, error) {
|
func ResolveIPWithResolver(host string, r Resolver) (net.IP, error) {
|
||||||
if node := DefaultHosts.Search(host); node != nil {
|
if node := DefaultHosts.Search(host); node != nil {
|
||||||
return node.Data.(net.IP), nil
|
ip := node.Data
|
||||||
|
return ip.Unmap().AsSlice(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if r != 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.
|
// DomainTrie contains the main logic for adding and searching nodes for domain segments.
|
||||||
// support wildcard domain (e.g *.google.com)
|
// support wildcard domain (e.g *.google.com)
|
||||||
type DomainTrie struct {
|
type DomainTrie[T comparable] struct {
|
||||||
root *Node
|
root *Node[T]
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidAndSplitDomain(domain string) ([]string, bool) {
|
func ValidAndSplitDomain(domain string) ([]string, bool) {
|
||||||
@ -51,7 +51,7 @@ func ValidAndSplitDomain(domain string) ([]string, bool) {
|
|||||||
// 3. subdomain.*.example.com
|
// 3. subdomain.*.example.com
|
||||||
// 4. .example.com
|
// 4. .example.com
|
||||||
// 5. +.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)
|
parts, valid := ValidAndSplitDomain(domain)
|
||||||
if !valid {
|
if !valid {
|
||||||
return ErrInvalidDomain
|
return ErrInvalidDomain
|
||||||
@ -68,13 +68,13 @@ func (t *DomainTrie) Insert(domain string, data any) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *DomainTrie) insert(parts []string, data any) {
|
func (t *DomainTrie[T]) insert(parts []string, data T) {
|
||||||
node := t.root
|
node := t.root
|
||||||
// reverse storage domain part to save space
|
// reverse storage domain part to save space
|
||||||
for i := len(parts) - 1; i >= 0; i-- {
|
for i := len(parts) - 1; i >= 0; i-- {
|
||||||
part := parts[i]
|
part := parts[i]
|
||||||
if !node.hasChild(part) {
|
if !node.hasChild(part) {
|
||||||
node.addChild(part, newNode(nil))
|
node.addChild(part, newNode(getZero[T]()))
|
||||||
}
|
}
|
||||||
|
|
||||||
node = node.getChild(part)
|
node = node.getChild(part)
|
||||||
@ -88,7 +88,7 @@ func (t *DomainTrie) insert(parts []string, data any) {
|
|||||||
// 1. static part
|
// 1. static part
|
||||||
// 2. wildcard domain
|
// 2. wildcard domain
|
||||||
// 2. dot 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)
|
parts, valid := ValidAndSplitDomain(domain)
|
||||||
if !valid || parts[0] == "" {
|
if !valid || parts[0] == "" {
|
||||||
return nil
|
return nil
|
||||||
@ -96,26 +96,26 @@ func (t *DomainTrie) Search(domain string) *Node {
|
|||||||
|
|
||||||
n := t.search(t.root, parts)
|
n := t.search(t.root, parts)
|
||||||
|
|
||||||
if n == nil || n.Data == nil {
|
if n == nil || n.Data == getZero[T]() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return n
|
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 {
|
if len(parts) == 0 {
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
if c := node.getChild(parts[len(parts)-1]); c != nil {
|
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
|
return n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if c := node.getChild(wildcard); c != nil {
|
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
|
return n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,6 +124,6 @@ func (t *DomainTrie) search(node *Node, parts []string) *Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new, empty Trie.
|
// New returns a new, empty Trie.
|
||||||
func New() *DomainTrie {
|
func New[T comparable]() *DomainTrie[T] {
|
||||||
return &DomainTrie{root: newNode(nil)}
|
return &DomainTrie[T]{root: newNode[T](getZero[T]())}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
package trie
|
package trie
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"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) {
|
func TestTrie_Basic(t *testing.T) {
|
||||||
tree := New()
|
tree := New[netip.Addr]()
|
||||||
domains := []string{
|
domains := []string{
|
||||||
"example.com",
|
"example.com",
|
||||||
"google.com",
|
"google.com",
|
||||||
@ -23,7 +23,7 @@ func TestTrie_Basic(t *testing.T) {
|
|||||||
|
|
||||||
node := tree.Search("example.com")
|
node := tree.Search("example.com")
|
||||||
assert.NotNil(t, node)
|
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.NotNil(t, tree.Insert("", localIP))
|
||||||
assert.Nil(t, tree.Search(""))
|
assert.Nil(t, tree.Search(""))
|
||||||
assert.NotNil(t, tree.Search("localhost"))
|
assert.NotNil(t, tree.Search("localhost"))
|
||||||
@ -31,7 +31,7 @@ func TestTrie_Basic(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTrie_Wildcard(t *testing.T) {
|
func TestTrie_Wildcard(t *testing.T) {
|
||||||
tree := New()
|
tree := New[netip.Addr]()
|
||||||
domains := []string{
|
domains := []string{
|
||||||
"*.example.com",
|
"*.example.com",
|
||||||
"sub.*.example.com",
|
"sub.*.example.com",
|
||||||
@ -64,7 +64,7 @@ func TestTrie_Wildcard(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTrie_Priority(t *testing.T) {
|
func TestTrie_Priority(t *testing.T) {
|
||||||
tree := New()
|
tree := New[int]()
|
||||||
domains := []string{
|
domains := []string{
|
||||||
".dev",
|
".dev",
|
||||||
"example.dev",
|
"example.dev",
|
||||||
@ -79,18 +79,18 @@ func TestTrie_Priority(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for idx, domain := range domains {
|
for idx, domain := range domains {
|
||||||
tree.Insert(domain, idx)
|
tree.Insert(domain, idx+1)
|
||||||
}
|
}
|
||||||
|
|
||||||
assertFn("test.dev", 0)
|
assertFn("test.dev", 1)
|
||||||
assertFn("foo.bar.dev", 0)
|
assertFn("foo.bar.dev", 1)
|
||||||
assertFn("example.dev", 1)
|
assertFn("example.dev", 2)
|
||||||
assertFn("foo.example.dev", 2)
|
assertFn("foo.example.dev", 3)
|
||||||
assertFn("test.example.dev", 3)
|
assertFn("test.example.dev", 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTrie_Boundary(t *testing.T) {
|
func TestTrie_Boundary(t *testing.T) {
|
||||||
tree := New()
|
tree := New[netip.Addr]()
|
||||||
tree.Insert("*.dev", localIP)
|
tree.Insert("*.dev", localIP)
|
||||||
|
|
||||||
assert.NotNil(t, tree.Insert(".", localIP))
|
assert.NotNil(t, tree.Insert(".", localIP))
|
||||||
@ -99,7 +99,7 @@ func TestTrie_Boundary(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTrie_WildcardBoundary(t *testing.T) {
|
func TestTrie_WildcardBoundary(t *testing.T) {
|
||||||
tree := New()
|
tree := New[netip.Addr]()
|
||||||
tree.Insert("+.*", localIP)
|
tree.Insert("+.*", localIP)
|
||||||
tree.Insert("stun.*.*.*", localIP)
|
tree.Insert("stun.*.*.*", localIP)
|
||||||
|
|
||||||
|
@ -1,26 +1,31 @@
|
|||||||
package trie
|
package trie
|
||||||
|
|
||||||
// Node is the trie's node
|
// Node is the trie's node
|
||||||
type Node struct {
|
type Node[T comparable] struct {
|
||||||
children map[string]*Node
|
children map[string]*Node[T]
|
||||||
Data any
|
Data T
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Node) getChild(s string) *Node {
|
func (n *Node[T]) getChild(s string) *Node[T] {
|
||||||
return n.children[s]
|
return n.children[s]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Node) hasChild(s string) bool {
|
func (n *Node[T]) hasChild(s string) bool {
|
||||||
return n.getChild(s) != nil
|
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
|
n.children[s] = child
|
||||||
}
|
}
|
||||||
|
|
||||||
func newNode(data any) *Node {
|
func newNode[T comparable](data T) *Node[T] {
|
||||||
return &Node{
|
return &Node[T]{
|
||||||
Data: data,
|
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/dns"
|
||||||
"github.com/Dreamacro/clash/listener/tun/ipstack/commons"
|
"github.com/Dreamacro/clash/listener/tun/ipstack/commons"
|
||||||
"github.com/Dreamacro/clash/log"
|
"github.com/Dreamacro/clash/log"
|
||||||
|
rewrites "github.com/Dreamacro/clash/rewrite"
|
||||||
R "github.com/Dreamacro/clash/rule"
|
R "github.com/Dreamacro/clash/rule"
|
||||||
T "github.com/Dreamacro/clash/tunnel"
|
T "github.com/Dreamacro/clash/tunnel"
|
||||||
|
|
||||||
@ -51,6 +52,7 @@ type Inbound struct {
|
|||||||
RedirPort int `json:"redir-port"`
|
RedirPort int `json:"redir-port"`
|
||||||
TProxyPort int `json:"tproxy-port"`
|
TProxyPort int `json:"tproxy-port"`
|
||||||
MixedPort int `json:"mixed-port"`
|
MixedPort int `json:"mixed-port"`
|
||||||
|
MitmPort int `json:"mitm-port"`
|
||||||
Authentication []string `json:"authentication"`
|
Authentication []string `json:"authentication"`
|
||||||
AllowLan bool `json:"allow-lan"`
|
AllowLan bool `json:"allow-lan"`
|
||||||
BindAddress string `json:"bind-address"`
|
BindAddress string `json:"bind-address"`
|
||||||
@ -74,7 +76,7 @@ type DNS struct {
|
|||||||
EnhancedMode C.DNSMode `yaml:"enhanced-mode"`
|
EnhancedMode C.DNSMode `yaml:"enhanced-mode"`
|
||||||
DefaultNameserver []dns.NameServer `yaml:"default-nameserver"`
|
DefaultNameserver []dns.NameServer `yaml:"default-nameserver"`
|
||||||
FakeIPRange *fakeip.Pool
|
FakeIPRange *fakeip.Pool
|
||||||
Hosts *trie.DomainTrie
|
Hosts *trie.DomainTrie[netip.Addr]
|
||||||
NameServerPolicy map[string]dns.NameServer
|
NameServerPolicy map[string]dns.NameServer
|
||||||
ProxyServerNameserver []dns.NameServer
|
ProxyServerNameserver []dns.NameServer
|
||||||
}
|
}
|
||||||
@ -115,6 +117,12 @@ type IPTables struct {
|
|||||||
InboundInterface string `yaml:"inbound-interface" json:"inbound-interface"`
|
InboundInterface string `yaml:"inbound-interface" json:"inbound-interface"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mitm config
|
||||||
|
type Mitm struct {
|
||||||
|
Hosts *trie.DomainTrie[bool] `yaml:"hosts" json:"hosts"`
|
||||||
|
Rules C.RewriteRule `yaml:"rules" json:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
// Experimental config
|
// Experimental config
|
||||||
type Experimental struct{}
|
type Experimental struct{}
|
||||||
|
|
||||||
@ -123,9 +131,10 @@ type Config struct {
|
|||||||
General *General
|
General *General
|
||||||
Tun *Tun
|
Tun *Tun
|
||||||
IPTables *IPTables
|
IPTables *IPTables
|
||||||
|
Mitm *Mitm
|
||||||
DNS *DNS
|
DNS *DNS
|
||||||
Experimental *Experimental
|
Experimental *Experimental
|
||||||
Hosts *trie.DomainTrie
|
Hosts *trie.DomainTrie[netip.Addr]
|
||||||
Profile *Profile
|
Profile *Profile
|
||||||
Rules []C.Rule
|
Rules []C.Rule
|
||||||
RuleProviders map[string]C.Rule
|
RuleProviders map[string]C.Rule
|
||||||
@ -166,12 +175,18 @@ type RawTun struct {
|
|||||||
AutoRoute bool `yaml:"auto-route" json:"auto-route"`
|
AutoRoute bool `yaml:"auto-route" json:"auto-route"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RawMitm struct {
|
||||||
|
Hosts []string `yaml:"hosts" json:"hosts"`
|
||||||
|
Rules []string `yaml:"rules" json:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
type RawConfig struct {
|
type RawConfig struct {
|
||||||
Port int `yaml:"port"`
|
Port int `yaml:"port"`
|
||||||
SocksPort int `yaml:"socks-port"`
|
SocksPort int `yaml:"socks-port"`
|
||||||
RedirPort int `yaml:"redir-port"`
|
RedirPort int `yaml:"redir-port"`
|
||||||
TProxyPort int `yaml:"tproxy-port"`
|
TProxyPort int `yaml:"tproxy-port"`
|
||||||
MixedPort int `yaml:"mixed-port"`
|
MixedPort int `yaml:"mixed-port"`
|
||||||
|
MitmPort int `yaml:"mitm-port"`
|
||||||
Authentication []string `yaml:"authentication"`
|
Authentication []string `yaml:"authentication"`
|
||||||
AllowLan bool `yaml:"allow-lan"`
|
AllowLan bool `yaml:"allow-lan"`
|
||||||
BindAddress string `yaml:"bind-address"`
|
BindAddress string `yaml:"bind-address"`
|
||||||
@ -189,6 +204,7 @@ type RawConfig struct {
|
|||||||
DNS RawDNS `yaml:"dns"`
|
DNS RawDNS `yaml:"dns"`
|
||||||
Tun RawTun `yaml:"tun"`
|
Tun RawTun `yaml:"tun"`
|
||||||
IPTables IPTables `yaml:"iptables"`
|
IPTables IPTables `yaml:"iptables"`
|
||||||
|
MITM RawMitm `yaml:"mitm"`
|
||||||
Experimental Experimental `yaml:"experimental"`
|
Experimental Experimental `yaml:"experimental"`
|
||||||
Profile Profile `yaml:"profile"`
|
Profile Profile `yaml:"profile"`
|
||||||
Proxy []map[string]any `yaml:"proxies"`
|
Proxy []map[string]any `yaml:"proxies"`
|
||||||
@ -250,6 +266,10 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
|
|||||||
"tls://223.5.5.5:853",
|
"tls://223.5.5.5:853",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
MITM: RawMitm{
|
||||||
|
Hosts: []string{},
|
||||||
|
Rules: []string{},
|
||||||
|
},
|
||||||
Profile: Profile{
|
Profile: Profile{
|
||||||
StoreSelected: true,
|
StoreSelected: true,
|
||||||
},
|
},
|
||||||
@ -314,6 +334,12 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
|
|||||||
}
|
}
|
||||||
config.DNS = dnsCfg
|
config.DNS = dnsCfg
|
||||||
|
|
||||||
|
mitm, err := parseMitm(rawCfg.MITM)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
config.Mitm = mitm
|
||||||
|
|
||||||
config.Users = parseAuthentication(rawCfg.Authentication)
|
config.Users = parseAuthentication(rawCfg.Authentication)
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
@ -338,6 +364,7 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
|
|||||||
RedirPort: cfg.RedirPort,
|
RedirPort: cfg.RedirPort,
|
||||||
TProxyPort: cfg.TProxyPort,
|
TProxyPort: cfg.TProxyPort,
|
||||||
MixedPort: cfg.MixedPort,
|
MixedPort: cfg.MixedPort,
|
||||||
|
MitmPort: cfg.MitmPort,
|
||||||
AllowLan: cfg.AllowLan,
|
AllowLan: cfg.AllowLan,
|
||||||
BindAddress: cfg.BindAddress,
|
BindAddress: cfg.BindAddress,
|
||||||
},
|
},
|
||||||
@ -546,24 +573,29 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[strin
|
|||||||
return rules, ruleProviders, nil
|
return rules, ruleProviders, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseHosts(cfg *RawConfig) (*trie.DomainTrie, error) {
|
func parseHosts(cfg *RawConfig) (*trie.DomainTrie[netip.Addr], error) {
|
||||||
tree := trie.New()
|
tree := trie.New[netip.Addr]()
|
||||||
|
|
||||||
// add default hosts
|
// add default hosts
|
||||||
if err := tree.Insert("localhost", net.IP{127, 0, 0, 1}); err != nil {
|
if err := tree.Insert("localhost", netip.AddrFrom4([4]byte{127, 0, 0, 1})); err != nil {
|
||||||
log.Errorln("insert localhost to host error: %s", err.Error())
|
log.Errorln("insert localhost to host error: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cfg.Hosts) != 0 {
|
if len(cfg.Hosts) != 0 {
|
||||||
for domain, ipStr := range cfg.Hosts {
|
for domain, ipStr := range cfg.Hosts {
|
||||||
ip := net.ParseIP(ipStr)
|
ip, err := netip.ParseAddr(ipStr)
|
||||||
if ip == nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s is not a valid IP", ipStr)
|
return nil, fmt.Errorf("%s is not a valid IP", ipStr)
|
||||||
}
|
}
|
||||||
_ = tree.Insert(domain, ip)
|
_ = tree.Insert(domain, ip)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add mitm.clash hosts
|
||||||
|
if err := tree.Insert("mitm.clash", netip.AddrFrom4([4]byte{8, 8, 9, 9})); err != nil {
|
||||||
|
log.Errorln("insert mitm.clash to host error: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
return tree, nil
|
return tree, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -697,7 +729,7 @@ func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]*router.DomainM
|
|||||||
return sites, nil
|
return sites, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS, error) {
|
func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[netip.Addr], rules []C.Rule) (*DNS, error) {
|
||||||
cfg := rawCfg.DNS
|
cfg := rawCfg.DNS
|
||||||
if cfg.Enable && len(cfg.NameServer) == 0 {
|
if cfg.Enable && len(cfg.NameServer) == 0 {
|
||||||
return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty")
|
return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty")
|
||||||
@ -750,10 +782,10 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var host *trie.DomainTrie
|
var host *trie.DomainTrie[bool]
|
||||||
// fake ip skip host filter
|
// fake ip skip host filter
|
||||||
if len(cfg.FakeIPFilter) != 0 {
|
if len(cfg.FakeIPFilter) != 0 {
|
||||||
host = trie.New()
|
host = trie.New[bool]()
|
||||||
for _, domain := range cfg.FakeIPFilter {
|
for _, domain := range cfg.FakeIPFilter {
|
||||||
_ = host.Insert(domain, true)
|
_ = host.Insert(domain, true)
|
||||||
}
|
}
|
||||||
@ -761,7 +793,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS,
|
|||||||
|
|
||||||
if len(dnsCfg.Fallback) != 0 {
|
if len(dnsCfg.Fallback) != 0 {
|
||||||
if host == nil {
|
if host == nil {
|
||||||
host = trie.New()
|
host = trie.New[bool]()
|
||||||
}
|
}
|
||||||
for _, fb := range dnsCfg.Fallback {
|
for _, fb := range dnsCfg.Fallback {
|
||||||
if net.ParseIP(fb.Addr) != nil {
|
if net.ParseIP(fb.Addr) != nil {
|
||||||
@ -930,3 +962,38 @@ func cleanPyKeywords(code string) string {
|
|||||||
}
|
}
|
||||||
return code
|
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
|
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) {
|
func downloadGeoSite(path string) (err error) {
|
||||||
resp, err := http.Get("https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat")
|
resp, err := http.Get("https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -84,19 +67,6 @@ func downloadGeoSite(path string) (err error) {
|
|||||||
return err
|
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 {
|
func initGeoSite() error {
|
||||||
if _, err := os.Stat(C.Path.GeoSite()); os.IsNotExist(err) {
|
if _, err := os.Stat(C.Path.GeoSite()); os.IsNotExist(err) {
|
||||||
log.Infoln("Can't find GeoSite.dat, start download")
|
log.Infoln("Can't find GeoSite.dat, start download")
|
||||||
@ -129,11 +99,6 @@ func Init(dir string) error {
|
|||||||
f.Close()
|
f.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
//// initial GeoIP
|
|
||||||
//if err := initGeoIP(); err != nil {
|
|
||||||
// return fmt.Errorf("can't initial GeoIP: %w", err)
|
|
||||||
//}
|
|
||||||
|
|
||||||
// initial mmdb
|
// initial mmdb
|
||||||
if err := initMMDB(); err != nil {
|
if err := initMMDB(); err != nil {
|
||||||
return fmt.Errorf("can't initial MMDB: %w", err)
|
return fmt.Errorf("can't initial MMDB: %w", err)
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
Direct AdapterType = iota
|
Direct AdapterType = iota
|
||||||
Reject
|
Reject
|
||||||
|
Mitm
|
||||||
|
|
||||||
Shadowsocks
|
Shadowsocks
|
||||||
ShadowsocksR
|
ShadowsocksR
|
||||||
@ -129,6 +130,8 @@ func (at AdapterType) String() string {
|
|||||||
return "Direct"
|
return "Direct"
|
||||||
case Reject:
|
case Reject:
|
||||||
return "Reject"
|
return "Reject"
|
||||||
|
case Mitm:
|
||||||
|
return "Mitm"
|
||||||
|
|
||||||
case Shadowsocks:
|
case Shadowsocks:
|
||||||
return "Shadowsocks"
|
return "Shadowsocks"
|
||||||
|
@ -23,6 +23,7 @@ const (
|
|||||||
REDIR
|
REDIR
|
||||||
TPROXY
|
TPROXY
|
||||||
TUN
|
TUN
|
||||||
|
MITM
|
||||||
)
|
)
|
||||||
|
|
||||||
type NetWork int
|
type NetWork int
|
||||||
@ -58,6 +59,8 @@ func (t Type) String() string {
|
|||||||
return "TProxy"
|
return "TProxy"
|
||||||
case TUN:
|
case TUN:
|
||||||
return "Tun"
|
return "Tun"
|
||||||
|
case MITM:
|
||||||
|
return "Mitm"
|
||||||
default:
|
default:
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
}
|
}
|
||||||
@ -80,10 +83,15 @@ type Metadata struct {
|
|||||||
DNSMode DNSMode `json:"dnsMode"`
|
DNSMode DNSMode `json:"dnsMode"`
|
||||||
Process string `json:"process"`
|
Process string `json:"process"`
|
||||||
ProcessPath string `json:"processPath"`
|
ProcessPath string `json:"processPath"`
|
||||||
|
UserAgent string `json:"userAgent"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Metadata) RemoteAddress() string {
|
func (m *Metadata) RemoteAddress() string {
|
||||||
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 {
|
func (m *Metadata) SourceAddress() string {
|
||||||
|
@ -98,6 +98,10 @@ func (p *path) GetExecutableFullPath() string {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *path) GetAssetLocation(file string) string {
|
func (p *path) RootCA() string {
|
||||||
return P.Join(p.homeDir, file)
|
return p.Resolve("mitm_ca.crt")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *path) CAKey() string {
|
||||||
|
return p.Resolve("mitm_ca.key")
|
||||||
}
|
}
|
||||||
|
82
constant/rewrite.go
Normal file
82
constant/rewrite.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var RewriteTypeMapping = map[string]RewriteType{
|
||||||
|
MitmReject.String(): MitmReject,
|
||||||
|
MitmReject200.String(): MitmReject200,
|
||||||
|
MitmRejectImg.String(): MitmRejectImg,
|
||||||
|
MitmRejectDict.String(): MitmRejectDict,
|
||||||
|
MitmRejectArray.String(): MitmRejectArray,
|
||||||
|
Mitm302.String(): Mitm302,
|
||||||
|
Mitm307.String(): Mitm307,
|
||||||
|
MitmRequestHeader.String(): MitmRequestHeader,
|
||||||
|
MitmRequestBody.String(): MitmRequestBody,
|
||||||
|
MitmResponseHeader.String(): MitmResponseHeader,
|
||||||
|
MitmResponseBody.String(): MitmResponseBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
MitmReject RewriteType = iota
|
||||||
|
MitmReject200
|
||||||
|
MitmRejectImg
|
||||||
|
MitmRejectDict
|
||||||
|
MitmRejectArray
|
||||||
|
|
||||||
|
Mitm302
|
||||||
|
Mitm307
|
||||||
|
|
||||||
|
MitmRequestHeader
|
||||||
|
MitmRequestBody
|
||||||
|
|
||||||
|
MitmResponseHeader
|
||||||
|
MitmResponseBody
|
||||||
|
)
|
||||||
|
|
||||||
|
type RewriteType int
|
||||||
|
|
||||||
|
func (rt RewriteType) String() string {
|
||||||
|
switch rt {
|
||||||
|
case MitmReject:
|
||||||
|
return "reject" // 404
|
||||||
|
case MitmReject200:
|
||||||
|
return "reject-200"
|
||||||
|
case MitmRejectImg:
|
||||||
|
return "reject-img"
|
||||||
|
case MitmRejectDict:
|
||||||
|
return "reject-dict"
|
||||||
|
case MitmRejectArray:
|
||||||
|
return "reject-array"
|
||||||
|
case Mitm302:
|
||||||
|
return "302"
|
||||||
|
case Mitm307:
|
||||||
|
return "307"
|
||||||
|
case MitmRequestHeader:
|
||||||
|
return "request-header"
|
||||||
|
case MitmRequestBody:
|
||||||
|
return "request-body"
|
||||||
|
case MitmResponseHeader:
|
||||||
|
return "response-header"
|
||||||
|
case MitmResponseBody:
|
||||||
|
return "response-body"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Rewrite interface {
|
||||||
|
ID() string
|
||||||
|
URLRegx() *regexp.Regexp
|
||||||
|
RuleType() RewriteType
|
||||||
|
RuleRegx() *regexp.Regexp
|
||||||
|
RulePayload() string
|
||||||
|
ReplaceURLPayload([]string) string
|
||||||
|
ReplaceSubPayload(string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RewriteRule interface {
|
||||||
|
SearchInRequest(func(Rewrite) bool) bool
|
||||||
|
SearchInResponse(func(Rewrite) bool) bool
|
||||||
|
}
|
@ -14,6 +14,7 @@ const (
|
|||||||
Process
|
Process
|
||||||
ProcessPath
|
ProcessPath
|
||||||
Script
|
Script
|
||||||
|
UserAgent
|
||||||
MATCH
|
MATCH
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -45,6 +46,8 @@ func (rt RuleType) String() string {
|
|||||||
return "ProcessPath"
|
return "ProcessPath"
|
||||||
case Script:
|
case Script:
|
||||||
return "Script"
|
return "Script"
|
||||||
|
case UserAgent:
|
||||||
|
return "UserAgent"
|
||||||
case MATCH:
|
case MATCH:
|
||||||
return "Match"
|
return "Match"
|
||||||
default:
|
default:
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
type ResolverEnhancer struct {
|
type ResolverEnhancer struct {
|
||||||
mode C.DNSMode
|
mode C.DNSMode
|
||||||
fakePool *fakeip.Pool
|
fakePool *fakeip.Pool
|
||||||
mapping *cache.LruCache
|
mapping *cache.LruCache[string, string]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ResolverEnhancer) FakeIPEnabled() bool {
|
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 mapping := h.mapping; mapping != nil {
|
||||||
if host, existed := h.mapping.Get(ip.String()); existed {
|
if host, existed := h.mapping.Get(ip.String()); existed {
|
||||||
return host.(string), true
|
return host, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", false
|
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) {
|
func (h *ResolverEnhancer) PatchFrom(o *ResolverEnhancer) {
|
||||||
if h.mapping != nil && o.mapping != nil {
|
if h.mapping != nil && o.mapping != nil {
|
||||||
o.mapping.CloneTo(h.mapping)
|
o.mapping.CloneTo(h.mapping)
|
||||||
@ -93,11 +99,11 @@ func (h *ResolverEnhancer) FlushFakeIP() error {
|
|||||||
|
|
||||||
func NewEnhancer(cfg Config) *ResolverEnhancer {
|
func NewEnhancer(cfg Config) *ResolverEnhancer {
|
||||||
var fakePool *fakeip.Pool
|
var fakePool *fakeip.Pool
|
||||||
var mapping *cache.LruCache
|
var mapping *cache.LruCache[string, string]
|
||||||
|
|
||||||
if cfg.EnhancedMode != C.DNSNormal {
|
if cfg.EnhancedMode != C.DNSNormal {
|
||||||
fakePool = cfg.Pool
|
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{
|
return &ResolverEnhancer{
|
||||||
|
@ -35,13 +35,13 @@ type fallbackDomainFilter interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type domainFilter struct {
|
type domainFilter struct {
|
||||||
tree *trie.DomainTrie
|
tree *trie.DomainTrie[bool]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDomainFilter(domains []string) *domainFilter {
|
func NewDomainFilter(domains []string) *domainFilter {
|
||||||
df := domainFilter{tree: trie.New()}
|
df := domainFilter{tree: trie.New[bool]()}
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
df.tree.Insert(domain, "")
|
df.tree.Insert(domain, true)
|
||||||
}
|
}
|
||||||
return &df
|
return &df
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package dns
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -20,7 +21,7 @@ type (
|
|||||||
middleware func(next handler) handler
|
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(next handler) handler {
|
||||||
return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) {
|
return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) {
|
||||||
q := r.Question[0]
|
q := r.Question[0]
|
||||||
@ -29,24 +30,29 @@ func withHosts(hosts *trie.DomainTrie) middleware {
|
|||||||
return next(ctx, r)
|
return next(ctx, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
record := hosts.Search(strings.TrimRight(q.Name, "."))
|
qName := strings.TrimRight(q.Name, ".")
|
||||||
|
record := hosts.Search(qName)
|
||||||
if record == nil {
|
if record == nil {
|
||||||
return next(ctx, r)
|
return next(ctx, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
ip := record.Data.(net.IP)
|
ip := record.Data
|
||||||
|
if mapping != nil {
|
||||||
|
mapping.SetWithExpire(ip.Unmap().String(), qName, time.Now().Add(time.Second*5))
|
||||||
|
}
|
||||||
|
|
||||||
msg := r.Copy()
|
msg := r.Copy()
|
||||||
|
|
||||||
if v4 := ip.To4(); v4 != nil && q.Qtype == D.TypeA {
|
if ip.Is4() && q.Qtype == D.TypeA {
|
||||||
rr := &D.A{}
|
rr := &D.A{}
|
||||||
rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeA, Class: D.ClassINET, Ttl: dnsDefaultTTL}
|
rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeA, Class: D.ClassINET, Ttl: 1}
|
||||||
rr.A = v4
|
rr.A = ip.AsSlice()
|
||||||
|
|
||||||
msg.Answer = []D.RR{rr}
|
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 := &D.AAAA{}
|
||||||
rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeAAAA, Class: D.ClassINET, Ttl: dnsDefaultTTL}
|
rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeAAAA, Class: D.ClassINET, Ttl: 1}
|
||||||
rr.AAAA = v6
|
rr.AAAA = ip.AsSlice()
|
||||||
|
|
||||||
msg.Answer = []D.RR{rr}
|
msg.Answer = []D.RR{rr}
|
||||||
} else {
|
} 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(next handler) handler {
|
||||||
return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) {
|
return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) {
|
||||||
q := r.Question[0]
|
q := r.Question[0]
|
||||||
@ -176,7 +182,7 @@ func NewHandler(resolver *Resolver, mapper *ResolverEnhancer) handler {
|
|||||||
middlewares := []middleware{}
|
middlewares := []middleware{}
|
||||||
|
|
||||||
if resolver.hosts != nil {
|
if resolver.hosts != nil {
|
||||||
middlewares = append(middlewares, withHosts(resolver.hosts))
|
middlewares = append(middlewares, withHosts(resolver.hosts, mapper.mapping))
|
||||||
}
|
}
|
||||||
|
|
||||||
if mapper.mode == C.DNSFakeIP {
|
if mapper.mode == C.DNSFakeIP {
|
||||||
|
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"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -33,14 +34,14 @@ type result struct {
|
|||||||
|
|
||||||
type Resolver struct {
|
type Resolver struct {
|
||||||
ipv6 bool
|
ipv6 bool
|
||||||
hosts *trie.DomainTrie
|
hosts *trie.DomainTrie[netip.Addr]
|
||||||
main []dnsClient
|
main []dnsClient
|
||||||
fallback []dnsClient
|
fallback []dnsClient
|
||||||
fallbackDomainFilters []fallbackDomainFilter
|
fallbackDomainFilters []fallbackDomainFilter
|
||||||
fallbackIPFilters []fallbackIPFilter
|
fallbackIPFilters []fallbackIPFilter
|
||||||
group singleflight.Group
|
group singleflight.Group
|
||||||
lruCache *cache.LruCache
|
lruCache *cache.LruCache[string, *D.Msg]
|
||||||
policy *trie.DomainTrie
|
policy *trie.DomainTrie[*Policy]
|
||||||
proxyServer []dnsClient
|
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())
|
cache, expireTime, hit := r.lruCache.GetWithExpire(q.String())
|
||||||
if hit {
|
if hit {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
msg = cache.(*D.Msg).Copy()
|
msg = cache.Copy()
|
||||||
if expireTime.Before(now) {
|
if expireTime.Before(now) {
|
||||||
setMsgTTL(msg, uint32(1)) // Continue fetch
|
setMsgTTL(msg, uint32(1)) // Continue fetch
|
||||||
go r.exchangeWithoutCache(ctx, m)
|
go r.exchangeWithoutCache(ctx, m)
|
||||||
@ -194,7 +195,8 @@ func (r *Resolver) matchPolicy(m *D.Msg) []dnsClient {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return record.Data.([]dnsClient)
|
p := record.Data
|
||||||
|
return p.GetData()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resolver) shouldOnlyQueryFallback(m *D.Msg) bool {
|
func (r *Resolver) shouldOnlyQueryFallback(m *D.Msg) bool {
|
||||||
@ -330,20 +332,20 @@ type Config struct {
|
|||||||
EnhancedMode C.DNSMode
|
EnhancedMode C.DNSMode
|
||||||
FallbackFilter FallbackFilter
|
FallbackFilter FallbackFilter
|
||||||
Pool *fakeip.Pool
|
Pool *fakeip.Pool
|
||||||
Hosts *trie.DomainTrie
|
Hosts *trie.DomainTrie[netip.Addr]
|
||||||
Policy map[string]NameServer
|
Policy map[string]NameServer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewResolver(config Config) *Resolver {
|
func NewResolver(config Config) *Resolver {
|
||||||
defaultResolver := &Resolver{
|
defaultResolver := &Resolver{
|
||||||
main: transform(config.Default, nil),
|
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{
|
r := &Resolver{
|
||||||
ipv6: config.IPv6,
|
ipv6: config.IPv6,
|
||||||
main: transform(config.Main, defaultResolver),
|
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,
|
hosts: config.Hosts,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -356,9 +358,9 @@ func NewResolver(config Config) *Resolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(config.Policy) != 0 {
|
if len(config.Policy) != 0 {
|
||||||
r.policy = trie.New()
|
r.policy = trie.New[*Policy]()
|
||||||
for domain, nameserver := range config.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"
|
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
|
var ttl uint32
|
||||||
switch {
|
switch {
|
||||||
case len(msg.Answer) != 0:
|
case len(msg.Answer) != 0:
|
||||||
|
10
go.mod
10
go.mod
@ -18,10 +18,11 @@ require (
|
|||||||
go.etcd.io/bbolt v1.3.6
|
go.etcd.io/bbolt v1.3.6
|
||||||
go.uber.org/atomic v1.9.0
|
go.uber.org/atomic v1.9.0
|
||||||
go.uber.org/automaxprocs v1.4.0
|
go.uber.org/automaxprocs v1.4.0
|
||||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd
|
golang.org/x/crypto v0.0.0-20220408190544-5352b0902921
|
||||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f
|
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||||
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86
|
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f
|
||||||
|
golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab
|
||||||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65
|
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20220318042302-193cf8d6a5d6
|
golang.zx2c4.com/wireguard v0.0.0-20220318042302-193cf8d6a5d6
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.4-0.20220317000008-6432784c2469
|
golang.zx2c4.com/wireguard/windows v0.5.4-0.20220317000008-6432784c2469
|
||||||
@ -37,8 +38,7 @@ require (
|
|||||||
github.com/oschwald/maxminddb-golang v1.8.0 // indirect
|
github.com/oschwald/maxminddb-golang v1.8.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect
|
github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect
|
||||||
golang.org/x/mod v0.5.1 // indirect
|
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 // indirect
|
||||||
golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab // indirect
|
|
||||||
golang.org/x/tools v0.1.9 // indirect
|
golang.org/x/tools v0.1.9 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
|
||||||
|
16
go.sum
16
go.sum
@ -81,11 +81,11 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38=
|
golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 h1:iU7T1X1J6yxDr0rda54sWGkHgOp5XJrqm79gcNlC2VM=
|
||||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220408190544-5352b0902921/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
|
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 h1:LQmS1nU0twXLA96Kt7U9qtHJEbBk3z6Q0V4UXjZkpr4=
|
||||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
@ -99,8 +99,8 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY
|
|||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
|
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 h1:EN5+DfgmRMvRUrMGERW2gQl3Vc+Z7ZMnI/xdEpPSf0c=
|
||||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@ -125,8 +125,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 h1:A9i04dxx7Cribqbs8jf3FQLogkL/CV2YN7hj9KWJCkc=
|
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw=
|
||||||
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
@ -3,12 +3,14 @@ package executor
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapter"
|
"github.com/Dreamacro/clash/adapter"
|
||||||
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
"github.com/Dreamacro/clash/adapter/outboundgroup"
|
"github.com/Dreamacro/clash/adapter/outboundgroup"
|
||||||
"github.com/Dreamacro/clash/component/auth"
|
"github.com/Dreamacro/clash/component/auth"
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
@ -72,13 +74,18 @@ func ApplyConfig(cfg *config.Config, force bool) {
|
|||||||
mux.Lock()
|
mux.Lock()
|
||||||
defer mux.Unlock()
|
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)
|
updateUsers(cfg.Users)
|
||||||
updateProxies(cfg.Proxies, cfg.Providers)
|
updateProxies(cfg.Proxies, cfg.Providers)
|
||||||
updateRules(cfg.Rules)
|
updateRules(cfg.Rules)
|
||||||
updateRuleProviders(cfg.RuleProviders)
|
updateRuleProviders(cfg.RuleProviders)
|
||||||
updateHosts(cfg.Hosts)
|
updateHosts(cfg.Hosts)
|
||||||
|
updateMitm(cfg.Mitm)
|
||||||
updateProfile(cfg)
|
updateProfile(cfg)
|
||||||
updateDNS(cfg.DNS, cfg.Tun)
|
updateDNS(cfg.DNS, cfg.Tun)
|
||||||
updateGeneral(cfg.General, force)
|
updateGeneral(cfg.General, force)
|
||||||
@ -103,6 +110,7 @@ func GetGeneral() *config.General {
|
|||||||
RedirPort: ports.RedirPort,
|
RedirPort: ports.RedirPort,
|
||||||
TProxyPort: ports.TProxyPort,
|
TProxyPort: ports.TProxyPort,
|
||||||
MixedPort: ports.MixedPort,
|
MixedPort: ports.MixedPort,
|
||||||
|
MitmPort: ports.MitmPort,
|
||||||
Authentication: authenticator,
|
Authentication: authenticator,
|
||||||
AllowLan: P.AllowLan(),
|
AllowLan: P.AllowLan(),
|
||||||
BindAddress: P.BindAddress(),
|
BindAddress: P.BindAddress(),
|
||||||
@ -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
|
resolver.DefaultHosts = tree
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,6 +239,7 @@ func updateGeneral(general *config.General, force bool) {
|
|||||||
P.ReCreateRedir(general.RedirPort, tcpIn, udpIn)
|
P.ReCreateRedir(general.RedirPort, tcpIn, udpIn)
|
||||||
P.ReCreateTProxy(general.TProxyPort, tcpIn, udpIn)
|
P.ReCreateTProxy(general.TProxyPort, tcpIn, udpIn)
|
||||||
P.ReCreateMixed(general.MixedPort, tcpIn, udpIn)
|
P.ReCreateMixed(general.MixedPort, tcpIn, udpIn)
|
||||||
|
P.ReCreateMitm(general.MitmPort, tcpIn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUsers(users []auth.AuthUser) {
|
func updateUsers(users []auth.AuthUser) {
|
||||||
@ -336,6 +345,11 @@ func updateIPTables(cfg *config.Config) {
|
|||||||
log.Infoln("[IPTABLES] Setting iptables completed")
|
log.Infoln("[IPTABLES] Setting iptables completed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateMitm(mitm *config.Mitm) {
|
||||||
|
outbound.MiddlemanRewriteHosts = mitm.Hosts
|
||||||
|
tunnel.UpdateRewrites(mitm.Rules)
|
||||||
|
}
|
||||||
|
|
||||||
func Shutdown() {
|
func Shutdown() {
|
||||||
P.Cleanup()
|
P.Cleanup()
|
||||||
S.Py_Finalize()
|
S.Py_Finalize()
|
||||||
|
@ -30,6 +30,7 @@ type configSchema struct {
|
|||||||
RedirPort *int `json:"redir-port"`
|
RedirPort *int `json:"redir-port"`
|
||||||
TProxyPort *int `json:"tproxy-port"`
|
TProxyPort *int `json:"tproxy-port"`
|
||||||
MixedPort *int `json:"mixed-port"`
|
MixedPort *int `json:"mixed-port"`
|
||||||
|
MitmPort *int `json:"mitm-port"`
|
||||||
Tun *config.Tun `json:"tun"`
|
Tun *config.Tun `json:"tun"`
|
||||||
AllowLan *bool `json:"allow-lan"`
|
AllowLan *bool `json:"allow-lan"`
|
||||||
BindAddress *string `json:"bind-address"`
|
BindAddress *string `json:"bind-address"`
|
||||||
@ -77,6 +78,7 @@ func patchConfigs(w http.ResponseWriter, r *http.Request) {
|
|||||||
P.ReCreateRedir(pointerOrDefault(general.RedirPort, ports.RedirPort), tcpIn, udpIn)
|
P.ReCreateRedir(pointerOrDefault(general.RedirPort, ports.RedirPort), tcpIn, udpIn)
|
||||||
P.ReCreateTProxy(pointerOrDefault(general.TProxyPort, ports.TProxyPort), tcpIn, udpIn)
|
P.ReCreateTProxy(pointerOrDefault(general.TProxyPort, ports.TProxyPort), tcpIn, udpIn)
|
||||||
P.ReCreateMixed(pointerOrDefault(general.MixedPort, ports.MixedPort), tcpIn, udpIn)
|
P.ReCreateMixed(pointerOrDefault(general.MixedPort, ports.MixedPort), tcpIn, udpIn)
|
||||||
|
P.ReCreateMitm(pointerOrDefault(general.MitmPort, ports.MitmPort), tcpIn)
|
||||||
|
|
||||||
if general.Mode != nil {
|
if general.Mode != nil {
|
||||||
tunnel.SetMode(*general.Mode)
|
tunnel.SetMode(*general.Mode)
|
||||||
|
@ -15,7 +15,7 @@ import (
|
|||||||
"github.com/Dreamacro/clash/log"
|
"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)
|
client := newClient(c.RemoteAddr(), in)
|
||||||
defer client.CloseIdleConnections()
|
defer client.CloseIdleConnections()
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.Cache) {
|
|||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
|
|
||||||
if !trusted {
|
if !trusted {
|
||||||
resp = authenticate(request, cache)
|
resp = Authenticate(request, cache)
|
||||||
|
|
||||||
trusted = resp == nil
|
trusted = resp == nil
|
||||||
}
|
}
|
||||||
@ -66,19 +66,19 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.Cache) {
|
|||||||
|
|
||||||
request.RequestURI = ""
|
request.RequestURI = ""
|
||||||
|
|
||||||
removeHopByHopHeaders(request.Header)
|
RemoveHopByHopHeaders(request.Header)
|
||||||
removeExtraHTTPHostPort(request)
|
RemoveExtraHTTPHostPort(request)
|
||||||
|
|
||||||
if request.URL.Scheme == "" || request.URL.Host == "" {
|
if request.URL.Scheme == "" || request.URL.Host == "" {
|
||||||
resp = responseWith(request, http.StatusBadRequest)
|
resp = ResponseWith(request, http.StatusBadRequest)
|
||||||
} else {
|
} else {
|
||||||
resp, err = client.Do(request)
|
resp, err = client.Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp = responseWith(request, http.StatusBadGateway)
|
resp = ResponseWith(request, http.StatusBadGateway)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
removeHopByHopHeaders(resp.Header)
|
RemoveHopByHopHeaders(resp.Header)
|
||||||
}
|
}
|
||||||
|
|
||||||
if keepAlive {
|
if keepAlive {
|
||||||
@ -98,33 +98,33 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.Cache) {
|
|||||||
conn.Close()
|
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()
|
authenticator := authStore.Authenticator()
|
||||||
if authenticator != nil {
|
if authenticator != nil {
|
||||||
credential := parseBasicProxyAuthorization(request)
|
credential := parseBasicProxyAuthorization(request)
|
||||||
if credential == "" {
|
if credential == "" {
|
||||||
resp := responseWith(request, http.StatusProxyAuthRequired)
|
resp := ResponseWith(request, http.StatusProxyAuthRequired)
|
||||||
resp.Header.Set("Proxy-Authenticate", "Basic")
|
resp.Header.Set("Proxy-Authenticate", "Basic")
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
var authed any
|
var authed bool
|
||||||
if authed = cache.Get(credential); authed == nil {
|
if authed = cache.Get(credential); !authed {
|
||||||
user, pass, err := decodeBasicProxyAuthorization(credential)
|
user, pass, err := decodeBasicProxyAuthorization(credential)
|
||||||
authed = err == nil && authenticator.Verify(user, pass)
|
authed = err == nil && authenticator.Verify(user, pass)
|
||||||
cache.Put(credential, authed, time.Minute)
|
cache.Put(credential, authed, time.Minute)
|
||||||
}
|
}
|
||||||
if !authed.(bool) {
|
if !authed {
|
||||||
log.Infoln("Auth failed from %s", request.RemoteAddr)
|
log.Infoln("Auth failed from %s", request.RemoteAddr)
|
||||||
|
|
||||||
return responseWith(request, http.StatusForbidden)
|
return ResponseWith(request, http.StatusForbidden)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func responseWith(request *http.Request, statusCode int) *http.Response {
|
func ResponseWith(request *http.Request, statusCode int) *http.Response {
|
||||||
return &http.Response{
|
return &http.Response{
|
||||||
StatusCode: statusCode,
|
StatusCode: statusCode,
|
||||||
Status: http.StatusText(statusCode),
|
Status: http.StatusText(statusCode),
|
||||||
|
@ -40,9 +40,9 @@ func NewWithAuthenticate(addr string, in chan<- C.ConnContext, authenticate bool
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var c *cache.Cache
|
var c *cache.Cache[string, bool]
|
||||||
if authenticate {
|
if authenticate {
|
||||||
c = cache.New(time.Second * 30)
|
c = cache.New[string, bool](time.Second * 30)
|
||||||
}
|
}
|
||||||
|
|
||||||
hl := &Listener{
|
hl := &Listener{
|
||||||
|
@ -8,8 +8,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// removeHopByHopHeaders remove hop-by-hop header
|
// RemoveHopByHopHeaders remove hop-by-hop header
|
||||||
func removeHopByHopHeaders(header http.Header) {
|
func RemoveHopByHopHeaders(header http.Header) {
|
||||||
// Strip hop-by-hop header based on RFC:
|
// Strip hop-by-hop header based on RFC:
|
||||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
|
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
|
||||||
// https://www.mnot.net/blog/2011/07/11/what_proxies_must_do
|
// https://www.mnot.net/blog/2011/07/11/what_proxies_must_do
|
||||||
@ -32,9 +32,9 @@ func removeHopByHopHeaders(header http.Header) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// removeExtraHTTPHostPort remove extra host port (example.com:80 --> example.com)
|
// RemoveExtraHTTPHostPort remove extra host port (example.com:80 --> example.com)
|
||||||
// It resolves the behavior of some HTTP servers that do not handle host:80 (e.g. baidu.com)
|
// It resolves the behavior of some HTTP servers that do not handle host:80 (e.g. baidu.com)
|
||||||
func removeExtraHTTPHostPort(req *http.Request) {
|
func RemoveExtraHTTPHostPort(req *http.Request) {
|
||||||
host := req.Host
|
host := req.Host
|
||||||
if host == "" {
|
if host == "" {
|
||||||
host = req.URL.Host
|
host = req.URL.Host
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
@ -8,10 +11,13 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapter/inbound"
|
"github.com/Dreamacro/clash/adapter/inbound"
|
||||||
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
|
"github.com/Dreamacro/clash/common/cert"
|
||||||
S "github.com/Dreamacro/clash/component/script"
|
S "github.com/Dreamacro/clash/component/script"
|
||||||
"github.com/Dreamacro/clash/config"
|
"github.com/Dreamacro/clash/config"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/listener/http"
|
"github.com/Dreamacro/clash/listener/http"
|
||||||
|
"github.com/Dreamacro/clash/listener/mitm"
|
||||||
"github.com/Dreamacro/clash/listener/mixed"
|
"github.com/Dreamacro/clash/listener/mixed"
|
||||||
"github.com/Dreamacro/clash/listener/redir"
|
"github.com/Dreamacro/clash/listener/redir"
|
||||||
"github.com/Dreamacro/clash/listener/socks"
|
"github.com/Dreamacro/clash/listener/socks"
|
||||||
@ -19,6 +25,8 @@ import (
|
|||||||
"github.com/Dreamacro/clash/listener/tun"
|
"github.com/Dreamacro/clash/listener/tun"
|
||||||
"github.com/Dreamacro/clash/listener/tun/ipstack"
|
"github.com/Dreamacro/clash/listener/tun/ipstack"
|
||||||
"github.com/Dreamacro/clash/log"
|
"github.com/Dreamacro/clash/log"
|
||||||
|
rewrites "github.com/Dreamacro/clash/rewrite"
|
||||||
|
"github.com/Dreamacro/clash/tunnel"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -35,6 +43,7 @@ var (
|
|||||||
mixedListener *mixed.Listener
|
mixedListener *mixed.Listener
|
||||||
mixedUDPLister *socks.UDPListener
|
mixedUDPLister *socks.UDPListener
|
||||||
tunStackListener ipstack.Stack
|
tunStackListener ipstack.Stack
|
||||||
|
mitmListener *mitm.Listener
|
||||||
|
|
||||||
// lock for recreate function
|
// lock for recreate function
|
||||||
socksMux sync.Mutex
|
socksMux sync.Mutex
|
||||||
@ -43,6 +52,7 @@ var (
|
|||||||
tproxyMux sync.Mutex
|
tproxyMux sync.Mutex
|
||||||
mixedMux sync.Mutex
|
mixedMux sync.Mutex
|
||||||
tunMux sync.Mutex
|
tunMux sync.Mutex
|
||||||
|
mitmMux sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
type Ports struct {
|
type Ports struct {
|
||||||
@ -51,6 +61,7 @@ type Ports struct {
|
|||||||
RedirPort int `json:"redir-port"`
|
RedirPort int `json:"redir-port"`
|
||||||
TProxyPort int `json:"tproxy-port"`
|
TProxyPort int `json:"tproxy-port"`
|
||||||
MixedPort int `json:"mixed-port"`
|
MixedPort int `json:"mixed-port"`
|
||||||
|
MitmPort int `json:"mitm-port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func AllowLan() bool {
|
func AllowLan() bool {
|
||||||
@ -333,6 +344,85 @@ func ReCreateTun(tunConf *config.Tun, tunAddressPrefix string, tcpIn chan<- C.Co
|
|||||||
tunStackListener, err = tun.New(tunConf, tunAddressPrefix, tcpIn, udpIn)
|
tunStackListener, err = tun.New(tunConf, tunAddressPrefix, tcpIn, udpIn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ReCreateMitm(port int, tcpIn chan<- C.ConnContext) {
|
||||||
|
mitmMux.Lock()
|
||||||
|
defer mitmMux.Unlock()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln("Start MITM server error: %s", err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
addr := genAddr(bindAddress, port, allowLan)
|
||||||
|
|
||||||
|
if mitmListener != nil {
|
||||||
|
if mitmListener.RawAddress() == addr {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
outbound.MiddlemanServerAddress.Store("")
|
||||||
|
tunnel.MitmOutbound = nil
|
||||||
|
_ = mitmListener.Close()
|
||||||
|
mitmListener = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if portIsZero(addr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = initCert(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
rootCACert tls.Certificate
|
||||||
|
x509c *x509.Certificate
|
||||||
|
certOption *cert.Config
|
||||||
|
)
|
||||||
|
|
||||||
|
rootCACert, err = tls.LoadX509KeyPair(C.Path.RootCA(), C.Path.CAKey())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKey := rootCACert.PrivateKey.(*rsa.PrivateKey)
|
||||||
|
|
||||||
|
x509c, err = x509.ParseCertificate(rootCACert.Certificate[0])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
certOption, err = cert.NewConfig(
|
||||||
|
x509c,
|
||||||
|
privateKey,
|
||||||
|
cert.NewAutoGCCertsStorage(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
certOption.SetValidity(cert.TTL << 3)
|
||||||
|
certOption.SetOrganization("Clash ManInTheMiddle Proxy Services")
|
||||||
|
|
||||||
|
opt := &mitm.Option{
|
||||||
|
Addr: addr,
|
||||||
|
ApiHost: "mitm.clash",
|
||||||
|
CertConfig: certOption,
|
||||||
|
Handler: &rewrites.RewriteHandler{},
|
||||||
|
}
|
||||||
|
|
||||||
|
mitmListener, err = mitm.New(opt, tcpIn)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
outbound.MiddlemanServerAddress.Store(mitmListener.Address())
|
||||||
|
tunnel.MitmOutbound = outbound.NewMitm()
|
||||||
|
|
||||||
|
log.Infoln("Mitm proxy listening at: %s", mitmListener.Address())
|
||||||
|
}
|
||||||
|
|
||||||
// GetPorts return the ports of proxy servers
|
// GetPorts return the ports of proxy servers
|
||||||
func GetPorts() *Ports {
|
func GetPorts() *Ports {
|
||||||
ports := &Ports{}
|
ports := &Ports{}
|
||||||
@ -367,6 +457,12 @@ func GetPorts() *Ports {
|
|||||||
ports.MixedPort = port
|
ports.MixedPort = port
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if mitmListener != nil {
|
||||||
|
_, portStr, _ := net.SplitHostPort(mitmListener.Address())
|
||||||
|
port, _ := strconv.Atoi(portStr)
|
||||||
|
ports.MitmPort = port
|
||||||
|
}
|
||||||
|
|
||||||
return ports
|
return ports
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -389,6 +485,19 @@ func genAddr(host string, port int, allowLan bool) string {
|
|||||||
return fmt.Sprintf("127.0.0.1:%d", port)
|
return fmt.Sprintf("127.0.0.1:%d", port)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initCert() error {
|
||||||
|
if _, err := os.Stat(C.Path.RootCA()); os.IsNotExist(err) {
|
||||||
|
log.Infoln("Can't find mitm_ca.crt, start generate")
|
||||||
|
err = cert.GenerateAndSave(C.Path.RootCA(), C.Path.CAKey())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Infoln("Generated CA private key and CA certificate finish")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func Cleanup() {
|
func Cleanup() {
|
||||||
if tunStackListener != nil {
|
if tunStackListener != nil {
|
||||||
_ = tunStackListener.Close()
|
_ = tunStackListener.Close()
|
||||||
|
54
listener/mitm/client.go
Normal file
54
listener/mitm/client.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package mitm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/adapter/inbound"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/transport/socks5"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrCertUnsupported = errors.New("tls: client cert unsupported")
|
||||||
|
|
||||||
|
func newClient(source net.Addr, userAgent string, in chan<- C.ConnContext) *http.Client {
|
||||||
|
return &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
// excepted HTTP/2
|
||||||
|
TLSNextProto: make(map[string]func(string, *tls.Conn) http.RoundTripper),
|
||||||
|
// from http.DefaultTransport
|
||||||
|
MaxIdleConns: 100,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
GetClientCertificate: func(info *tls.CertificateRequestInfo) (certificate *tls.Certificate, e error) {
|
||||||
|
return nil, ErrCertUnsupported
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DialContext: func(context context.Context, network, address string) (net.Conn, error) {
|
||||||
|
if network != "tcp" && network != "tcp4" && network != "tcp6" {
|
||||||
|
return nil, errors.New("unsupported network " + network)
|
||||||
|
}
|
||||||
|
|
||||||
|
dstAddr := socks5.ParseAddr(address)
|
||||||
|
if dstAddr == nil {
|
||||||
|
return nil, socks5.ErrAddressNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
left, right := net.Pipe()
|
||||||
|
|
||||||
|
in <- inbound.NewMitm(dstAddr, source, userAgent, right)
|
||||||
|
|
||||||
|
return left, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
357
listener/mitm/proxy.go
Normal file
357
listener/mitm/proxy.go
Normal file
@ -0,0 +1,357 @@
|
|||||||
|
package mitm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/adapter/inbound"
|
||||||
|
"github.com/Dreamacro/clash/common/cache"
|
||||||
|
N "github.com/Dreamacro/clash/common/net"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
httpL "github.com/Dreamacro/clash/listener/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleConn(c net.Conn, opt *Option, in chan<- C.ConnContext, cache *cache.Cache[string, bool]) {
|
||||||
|
var (
|
||||||
|
source net.Addr
|
||||||
|
client *http.Client
|
||||||
|
)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if client != nil {
|
||||||
|
client.CloseIdleConnections()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
startOver:
|
||||||
|
if tc, ok := c.(*net.TCPConn); ok {
|
||||||
|
_ = tc.SetKeepAlive(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
var conn *N.BufferedConn
|
||||||
|
if bufConn, ok := c.(*N.BufferedConn); ok {
|
||||||
|
conn = bufConn
|
||||||
|
} else {
|
||||||
|
conn = N.NewBufferedConn(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
trusted := cache == nil // disable authenticate if cache is nil
|
||||||
|
|
||||||
|
readLoop:
|
||||||
|
for {
|
||||||
|
_ = conn.SetDeadline(time.Now().Add(30 * time.Second)) // use SetDeadline instead of Proxy-Connection keep-alive
|
||||||
|
|
||||||
|
request, err := httpL.ReadRequest(conn.Reader())
|
||||||
|
if err != nil {
|
||||||
|
handleError(opt, nil, err)
|
||||||
|
break readLoop
|
||||||
|
}
|
||||||
|
|
||||||
|
var response *http.Response
|
||||||
|
|
||||||
|
session := NewSession(conn, request, response)
|
||||||
|
|
||||||
|
source = parseSourceAddress(session.request, c, source)
|
||||||
|
request.RemoteAddr = source.String()
|
||||||
|
|
||||||
|
if !trusted {
|
||||||
|
response = httpL.Authenticate(request, cache)
|
||||||
|
|
||||||
|
trusted = response == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if trusted {
|
||||||
|
if session.request.Method == http.MethodConnect {
|
||||||
|
// Manual writing to support CONNECT for http 1.0 (workaround for uplay client)
|
||||||
|
if _, err = fmt.Fprintf(session.conn, "HTTP/%d.%d %03d %s\r\n\r\n", session.request.ProtoMajor, session.request.ProtoMinor, http.StatusOK, "Connection established"); err != nil {
|
||||||
|
handleError(opt, session, err)
|
||||||
|
break readLoop // close connection
|
||||||
|
}
|
||||||
|
|
||||||
|
if couldBeWithManInTheMiddleAttack(session.request.URL.Host, opt) {
|
||||||
|
b := make([]byte, 1)
|
||||||
|
if _, err = session.conn.Read(b); err != nil {
|
||||||
|
handleError(opt, session, err)
|
||||||
|
break readLoop // close connection
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, session.conn.(*N.BufferedConn).Buffered())
|
||||||
|
_, _ = session.conn.Read(buf)
|
||||||
|
|
||||||
|
mc := &MultiReaderConn{
|
||||||
|
Conn: session.conn,
|
||||||
|
reader: io.MultiReader(bytes.NewReader(b), bytes.NewReader(buf), session.conn),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 22 is the TLS handshake.
|
||||||
|
// https://tools.ietf.org/html/rfc5246#section-6.2.1
|
||||||
|
if b[0] == 22 {
|
||||||
|
// TODO serve by generic host name maybe better?
|
||||||
|
tlsConn := tls.Server(mc, opt.CertConfig.NewTLSConfigForHost(session.request.URL.Host))
|
||||||
|
|
||||||
|
// Handshake with the local client
|
||||||
|
if err = tlsConn.Handshake(); err != nil {
|
||||||
|
handleError(opt, session, err)
|
||||||
|
break readLoop // close connection
|
||||||
|
}
|
||||||
|
|
||||||
|
c = tlsConn
|
||||||
|
goto startOver // hijack and decrypt tls connection
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybe it's the others encrypted connection
|
||||||
|
in <- inbound.NewHTTPS(request, mc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybe it's a http connection
|
||||||
|
goto readLoop
|
||||||
|
}
|
||||||
|
|
||||||
|
// hijack api
|
||||||
|
if getHostnameWithoutPort(session.request) == opt.ApiHost {
|
||||||
|
if err = handleApiRequest(session, opt); err != nil {
|
||||||
|
handleError(opt, session, err)
|
||||||
|
break readLoop
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareRequest(c, session.request)
|
||||||
|
|
||||||
|
// hijack custom request and write back custom response if necessary
|
||||||
|
if opt.Handler != nil {
|
||||||
|
newReq, newRes := opt.Handler.HandleRequest(session)
|
||||||
|
if newReq != nil {
|
||||||
|
session.request = newReq
|
||||||
|
}
|
||||||
|
if newRes != nil {
|
||||||
|
session.response = newRes
|
||||||
|
|
||||||
|
if err = writeResponse(session, false); err != nil {
|
||||||
|
handleError(opt, session, err)
|
||||||
|
break readLoop
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
httpL.RemoveHopByHopHeaders(session.request.Header)
|
||||||
|
httpL.RemoveExtraHTTPHostPort(request)
|
||||||
|
|
||||||
|
session.request.RequestURI = ""
|
||||||
|
|
||||||
|
if session.request.URL.Scheme == "" || session.request.URL.Host == "" {
|
||||||
|
session.response = session.NewErrorResponse(errors.New("invalid URL"))
|
||||||
|
} else {
|
||||||
|
client = newClientBySourceAndUserAgentIfNil(client, session.request, source, in)
|
||||||
|
|
||||||
|
// send the request to remote server
|
||||||
|
session.response, err = client.Do(session.request)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
handleError(opt, session, err)
|
||||||
|
session.response = session.NewErrorResponse(err)
|
||||||
|
if errors.Is(err, ErrCertUnsupported) || strings.Contains(err.Error(), "x509: ") {
|
||||||
|
// TODO block unsupported host?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = writeResponseWithHandler(session, opt); err != nil {
|
||||||
|
handleError(opt, session, err)
|
||||||
|
break readLoop // close connection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeResponseWithHandler(session *Session, opt *Option) error {
|
||||||
|
if opt.Handler != nil {
|
||||||
|
res := opt.Handler.HandleResponse(session)
|
||||||
|
|
||||||
|
if res != nil {
|
||||||
|
body := res.Body
|
||||||
|
defer func(body io.ReadCloser) {
|
||||||
|
_ = body.Close()
|
||||||
|
}(body)
|
||||||
|
|
||||||
|
session.response = res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return writeResponse(session, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeResponse(session *Session, keepAlive bool) error {
|
||||||
|
httpL.RemoveHopByHopHeaders(session.response.Header)
|
||||||
|
|
||||||
|
if keepAlive {
|
||||||
|
session.response.Header.Set("Connection", "keep-alive")
|
||||||
|
session.response.Header.Set("Keep-Alive", "timeout=25")
|
||||||
|
}
|
||||||
|
|
||||||
|
// session.response.Close = !keepAlive // let handler do it
|
||||||
|
|
||||||
|
return session.response.Write(session.conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleApiRequest(session *Session, opt *Option) error {
|
||||||
|
if opt.CertConfig != nil && strings.ToLower(session.request.URL.Path) == "/cert.crt" {
|
||||||
|
b := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: opt.CertConfig.GetCA().Raw,
|
||||||
|
})
|
||||||
|
|
||||||
|
session.response = session.NewResponse(http.StatusOK, bytes.NewReader(b))
|
||||||
|
|
||||||
|
defer func(body io.ReadCloser) {
|
||||||
|
_ = body.Close()
|
||||||
|
}(session.response.Body)
|
||||||
|
|
||||||
|
session.response.Close = true
|
||||||
|
session.response.Header.Set("Content-Type", "application/x-x509-ca-cert")
|
||||||
|
session.response.ContentLength = int64(len(b))
|
||||||
|
|
||||||
|
return session.response.Write(session.conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
b := `<!DOCTYPE HTML PUBLIC "-
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Clash ManInTheMiddle Proxy Services - 404 Not Found</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Not Found</h1>
|
||||||
|
<p>The requested URL %s was not found on this server.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
if opt.Handler != nil {
|
||||||
|
if opt.Handler.HandleApiRequest(session) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b = fmt.Sprintf(b, session.request.URL.Path)
|
||||||
|
|
||||||
|
session.response = session.NewResponse(http.StatusNotFound, bytes.NewReader([]byte(b)))
|
||||||
|
|
||||||
|
defer func(body io.ReadCloser) {
|
||||||
|
_ = body.Close()
|
||||||
|
}(session.response.Body)
|
||||||
|
|
||||||
|
session.response.Close = true
|
||||||
|
session.response.Header.Set("Content-Type", "text/html;charset=utf-8")
|
||||||
|
session.response.ContentLength = int64(len(b))
|
||||||
|
|
||||||
|
return session.response.Write(session.conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleError(opt *Option, session *Session, err error) {
|
||||||
|
if opt.Handler != nil {
|
||||||
|
opt.Handler.HandleError(session, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// log.Errorln("[MITM] process mitm error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareRequest(conn net.Conn, request *http.Request) {
|
||||||
|
host := request.Header.Get("Host")
|
||||||
|
if host != "" {
|
||||||
|
request.Host = host
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.URL.Host == "" {
|
||||||
|
request.URL.Host = request.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
request.URL.Scheme = "http"
|
||||||
|
|
||||||
|
if tlsConn, ok := conn.(*tls.Conn); ok {
|
||||||
|
cs := tlsConn.ConnectionState()
|
||||||
|
request.TLS = &cs
|
||||||
|
|
||||||
|
request.URL.Scheme = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.Header.Get("Accept-Encoding") != "" {
|
||||||
|
request.Header.Set("Accept-Encoding", "gzip")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func couldBeWithManInTheMiddleAttack(hostname string, opt *Option) bool {
|
||||||
|
if opt.CertConfig == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, port, err := net.SplitHostPort(hostname); err == nil && (port == "443" || port == "8443") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHostnameWithoutPort(req *http.Request) string {
|
||||||
|
host := req.Host
|
||||||
|
if host == "" {
|
||||||
|
host = req.URL.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
if pHost, _, err := net.SplitHostPort(host); err == nil {
|
||||||
|
host = pHost
|
||||||
|
}
|
||||||
|
|
||||||
|
return host
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSourceAddress(req *http.Request, c net.Conn, source net.Addr) net.Addr {
|
||||||
|
if source != nil {
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceAddress := req.Header.Get("Origin-Request-Source-Address")
|
||||||
|
if sourceAddress == "" {
|
||||||
|
return c.RemoteAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Del("Origin-Request-Source-Address")
|
||||||
|
|
||||||
|
host, port, err := net.SplitHostPort(sourceAddress)
|
||||||
|
if err != nil {
|
||||||
|
return c.RemoteAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := strconv.ParseUint(port, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return c.RemoteAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip := net.ParseIP(host); ip != nil {
|
||||||
|
return &net.TCPAddr{
|
||||||
|
IP: ip,
|
||||||
|
Port: int(p),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.RemoteAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newClientBySourceAndUserAgentIfNil(cli *http.Client, req *http.Request, source net.Addr, in chan<- C.ConnContext) *http.Client {
|
||||||
|
if cli != nil {
|
||||||
|
return cli
|
||||||
|
}
|
||||||
|
|
||||||
|
return newClient(source, req.Header.Get("User-Agent"), in)
|
||||||
|
}
|
90
listener/mitm/server.go
Normal file
90
listener/mitm/server.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package mitm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/cache"
|
||||||
|
"github.com/Dreamacro/clash/common/cert"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler interface {
|
||||||
|
HandleRequest(*Session) (*http.Request, *http.Response) // Session.Response maybe nil
|
||||||
|
HandleResponse(*Session) *http.Response
|
||||||
|
HandleApiRequest(*Session) bool
|
||||||
|
HandleError(*Session, error) // Session maybe nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option struct {
|
||||||
|
Addr string
|
||||||
|
ApiHost string
|
||||||
|
|
||||||
|
TLSConfig *tls.Config
|
||||||
|
CertConfig *cert.Config
|
||||||
|
|
||||||
|
Handler Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
type Listener struct {
|
||||||
|
*Option
|
||||||
|
|
||||||
|
listener net.Listener
|
||||||
|
addr string
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// RawAddress implements C.Listener
|
||||||
|
func (l *Listener) RawAddress() string {
|
||||||
|
return l.addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address implements C.Listener
|
||||||
|
func (l *Listener) Address() string {
|
||||||
|
return l.listener.Addr().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements C.Listener
|
||||||
|
func (l *Listener) Close() error {
|
||||||
|
l.closed = true
|
||||||
|
return l.listener.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// New the MITM proxy actually is a type of HTTP proxy
|
||||||
|
func New(option *Option, in chan<- C.ConnContext) (*Listener, error) {
|
||||||
|
return NewWithAuthenticate(option, in, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWithAuthenticate(option *Option, in chan<- C.ConnContext, authenticate bool) (*Listener, error) {
|
||||||
|
l, err := net.Listen("tcp", option.Addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var c *cache.Cache[string, bool]
|
||||||
|
if authenticate {
|
||||||
|
c = cache.New[string, bool](time.Second * 30)
|
||||||
|
}
|
||||||
|
|
||||||
|
hl := &Listener{
|
||||||
|
listener: l,
|
||||||
|
addr: option.Addr,
|
||||||
|
Option: option,
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
conn, err1 := hl.listener.Accept()
|
||||||
|
if err1 != nil {
|
||||||
|
if hl.closed {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
go HandleConn(conn, option, in, c)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return hl, nil
|
||||||
|
}
|
56
listener/mitm/session.go
Normal file
56
listener/mitm/session.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package mitm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
var serverName = fmt.Sprintf("Clash server (%s)", C.Version)
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
conn net.Conn
|
||||||
|
request *http.Request
|
||||||
|
response *http.Response
|
||||||
|
|
||||||
|
props map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Request() *http.Request {
|
||||||
|
return s.request
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Response() *http.Response {
|
||||||
|
return s.response
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) GetProperties(key string) (any, bool) {
|
||||||
|
v, ok := s.props[key]
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) SetProperties(key string, val any) {
|
||||||
|
s.props[key] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) NewResponse(code int, body io.Reader) *http.Response {
|
||||||
|
res := NewResponse(code, body, s.request)
|
||||||
|
res.Header.Set("Server", serverName)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) NewErrorResponse(err error) *http.Response {
|
||||||
|
return NewErrorResponse(s.request, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSession(conn net.Conn, request *http.Request, response *http.Response) *Session {
|
||||||
|
return &Session{
|
||||||
|
conn: conn,
|
||||||
|
request: request,
|
||||||
|
response: response,
|
||||||
|
props: map[string]any{},
|
||||||
|
}
|
||||||
|
}
|
100
listener/mitm/utils.go
Normal file
100
listener/mitm/utils.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package mitm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/text/encoding/charmap"
|
||||||
|
"golang.org/x/text/transform"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MultiReaderConn struct {
|
||||||
|
net.Conn
|
||||||
|
reader io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiReaderConn) Read(buf []byte) (int, error) {
|
||||||
|
return c.reader.Read(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewResponse(code int, body io.Reader, req *http.Request) *http.Response {
|
||||||
|
if body == nil {
|
||||||
|
body = &bytes.Buffer{}
|
||||||
|
}
|
||||||
|
|
||||||
|
rc, ok := body.(io.ReadCloser)
|
||||||
|
if !ok {
|
||||||
|
rc = ioutil.NopCloser(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &http.Response{
|
||||||
|
StatusCode: code,
|
||||||
|
Status: fmt.Sprintf("%d %s", code, http.StatusText(code)),
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
ProtoMajor: 1,
|
||||||
|
ProtoMinor: 1,
|
||||||
|
Header: http.Header{},
|
||||||
|
Body: rc,
|
||||||
|
Request: req,
|
||||||
|
}
|
||||||
|
|
||||||
|
if req != nil {
|
||||||
|
res.Close = req.Close
|
||||||
|
res.Proto = req.Proto
|
||||||
|
res.ProtoMajor = req.ProtoMajor
|
||||||
|
res.ProtoMinor = req.ProtoMinor
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewErrorResponse(req *http.Request, err error) *http.Response {
|
||||||
|
res := NewResponse(http.StatusBadGateway, nil, req)
|
||||||
|
res.Close = true
|
||||||
|
|
||||||
|
date := res.Header.Get("Date")
|
||||||
|
if date == "" {
|
||||||
|
date = time.Now().Format(http.TimeFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
w := fmt.Sprintf(`199 "clash" %q %q`, err.Error(), date)
|
||||||
|
res.Header.Add("Warning", w)
|
||||||
|
res.Header.Set("Server", serverName)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadDecompressedBody(res *http.Response) ([]byte, error) {
|
||||||
|
rBody := res.Body
|
||||||
|
if res.Header.Get("Content-Encoding") == "gzip" {
|
||||||
|
gzReader, err := gzip.NewReader(rBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rBody = gzReader
|
||||||
|
|
||||||
|
defer func(gzReader *gzip.Reader) {
|
||||||
|
_ = gzReader.Close()
|
||||||
|
}(gzReader)
|
||||||
|
}
|
||||||
|
return ioutil.ReadAll(rBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeLatin1(reader io.Reader) (string, error) {
|
||||||
|
r := transform.NewReader(reader, charmap.ISO8859_1.NewDecoder())
|
||||||
|
b, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodeLatin1(str string) ([]byte, error) {
|
||||||
|
return charmap.ISO8859_1.NewEncoder().Bytes([]byte(str))
|
||||||
|
}
|
@ -16,7 +16,7 @@ import (
|
|||||||
type Listener struct {
|
type Listener struct {
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
addr string
|
addr string
|
||||||
cache *cache.Cache
|
cache *cache.Cache[string, bool]
|
||||||
closed bool
|
closed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ func New(addr string, in chan<- C.ConnContext) (*Listener, error) {
|
|||||||
ml := &Listener{
|
ml := &Listener{
|
||||||
listener: l,
|
listener: l,
|
||||||
addr: addr,
|
addr: addr,
|
||||||
cache: cache.New(30 * time.Second),
|
cache: cache.New[string, bool](30 * time.Second),
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
@ -63,7 +63,7 @@ func New(addr string, in chan<- C.ConnContext) (*Listener, error) {
|
|||||||
return ml, nil
|
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)
|
conn.(*net.TCPConn).SetKeepAlive(true)
|
||||||
|
|
||||||
bufConn := N.NewBufferedConn(conn)
|
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)
|
parsed, parseErr = NewProcess(payload, target, false)
|
||||||
case "SCRIPT":
|
case "SCRIPT":
|
||||||
parsed, parseErr = NewScript(payload, target)
|
parsed, parseErr = NewScript(payload, target)
|
||||||
|
case "USER-AGENT":
|
||||||
|
parsed, parseErr = NewUserAgent(payload, target)
|
||||||
case "MATCH":
|
case "MATCH":
|
||||||
parsed = NewMatch(target)
|
parsed = NewMatch(target)
|
||||||
default:
|
default:
|
||||||
|
52
rule/user_gent.go
Normal file
52
rule/user_gent.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package rules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserAgent struct {
|
||||||
|
*Base
|
||||||
|
ua string
|
||||||
|
adapter string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *UserAgent) RuleType() C.RuleType {
|
||||||
|
return C.UserAgent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *UserAgent) Match(metadata *C.Metadata) bool {
|
||||||
|
if metadata.Type != C.MITM || metadata.UserAgent == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Contains(metadata.UserAgent, d.ua)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *UserAgent) Adapter() string {
|
||||||
|
return d.adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *UserAgent) Payload() string {
|
||||||
|
return d.ua
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *UserAgent) ShouldResolveIP() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserAgent(ua string, adapter string) (*UserAgent, error) {
|
||||||
|
ua = strings.Trim(ua, "*")
|
||||||
|
if ua == "" {
|
||||||
|
return nil, errPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
return &UserAgent{
|
||||||
|
Base: &Base{},
|
||||||
|
ua: ua,
|
||||||
|
adapter: adapter,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ C.Rule = (*UserAgent)(nil)
|
@ -8,7 +8,7 @@ require (
|
|||||||
github.com/docker/go-connections v0.4.0
|
github.com/docker/go-connections v0.4.0
|
||||||
github.com/miekg/dns v1.1.47
|
github.com/miekg/dns v1.1.47
|
||||||
github.com/stretchr/testify v1.7.1
|
github.com/stretchr/testify v1.7.1
|
||||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f
|
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/Dreamacro/clash => ../
|
replace github.com/Dreamacro/clash => ../
|
||||||
@ -39,10 +39,10 @@ require (
|
|||||||
github.com/xtls/go v0.0.0-20210920065950-d4af136d3672 // indirect
|
github.com/xtls/go v0.0.0-20210920065950-d4af136d3672 // indirect
|
||||||
go.etcd.io/bbolt v1.3.6 // indirect
|
go.etcd.io/bbolt v1.3.6 // indirect
|
||||||
go.uber.org/atomic v1.9.0 // indirect
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect
|
golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 // indirect
|
||||||
golang.org/x/mod v0.5.1 // indirect
|
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 // indirect
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||||
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 // indirect
|
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect
|
||||||
golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab // indirect
|
golang.org/x/text v0.3.8-0.20220124021120-d1c84af989ab // indirect
|
||||||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
|
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
|
||||||
golang.org/x/tools v0.1.9 // indirect
|
golang.org/x/tools v0.1.9 // indirect
|
||||||
|
16
test/go.sum
16
test/go.sum
@ -913,8 +913,8 @@ golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWP
|
|||||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38=
|
golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 h1:iU7T1X1J6yxDr0rda54sWGkHgOp5XJrqm79gcNlC2VM=
|
||||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220408190544-5352b0902921/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@ -950,8 +950,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
|
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 h1:LQmS1nU0twXLA96Kt7U9qtHJEbBk3z6Q0V4UXjZkpr4=
|
||||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@ -1012,8 +1012,8 @@ golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||||||
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
|
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 h1:EN5+DfgmRMvRUrMGERW2gQl3Vc+Z7ZMnI/xdEpPSf0c=
|
||||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@ -1144,8 +1144,8 @@ golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 h1:A9i04dxx7Cribqbs8jf3FQLogkL/CV2YN7hj9KWJCkc=
|
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw=
|
||||||
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
@ -148,7 +148,7 @@ func (t *Trojan) PresetXTLSConn(conn net.Conn) (net.Conn, error) {
|
|||||||
xtlsConn.DirectMode = true
|
xtlsConn.DirectMode = true
|
||||||
}
|
}
|
||||||
} else {
|
} 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()
|
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()
|
uuid, _ := uuid.NewV4()
|
||||||
|
|
||||||
t := &tcpTracker{
|
t := &tcpTracker{
|
||||||
@ -80,7 +80,7 @@ func NewTCPTracker(conn C.Conn, manager *Manager, metadata *C.Metadata, rule C.R
|
|||||||
}
|
}
|
||||||
|
|
||||||
manager.Join(t)
|
manager.Join(t)
|
||||||
return t
|
return NewSniffing(t, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
type udpTracker struct {
|
type udpTracker struct {
|
||||||
|
@ -27,6 +27,7 @@ var (
|
|||||||
udpQueue = make(chan *inbound.PacketAdapter, 200)
|
udpQueue = make(chan *inbound.PacketAdapter, 200)
|
||||||
natTable = nat.New()
|
natTable = nat.New()
|
||||||
rules []C.Rule
|
rules []C.Rule
|
||||||
|
rewrites C.RewriteRule
|
||||||
proxies = make(map[string]C.Proxy)
|
proxies = make(map[string]C.Proxy)
|
||||||
providers map[string]provider.ProxyProvider
|
providers map[string]provider.ProxyProvider
|
||||||
configMux sync.RWMutex
|
configMux sync.RWMutex
|
||||||
@ -36,6 +37,9 @@ var (
|
|||||||
|
|
||||||
// default timeout for UDP session
|
// default timeout for UDP session
|
||||||
udpTimeout = 60 * time.Second
|
udpTimeout = 60 * time.Second
|
||||||
|
|
||||||
|
// MitmOutbound mitm proxy adapter
|
||||||
|
MitmOutbound C.ProxyAdapter
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -92,6 +96,18 @@ func SetMode(m TunnelMode) {
|
|||||||
mode = m
|
mode = m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rewrites return all rewrites
|
||||||
|
func Rewrites() C.RewriteRule {
|
||||||
|
return rewrites
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRewrites handle update rewrites
|
||||||
|
func UpdateRewrites(newRewrites C.RewriteRule) {
|
||||||
|
configMux.Lock()
|
||||||
|
rewrites = newRewrites
|
||||||
|
configMux.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// processUDP starts a loop to handle udp packet
|
// processUDP starts a loop to handle udp packet
|
||||||
func processUDP() {
|
func processUDP() {
|
||||||
queue := udpQueue
|
queue := udpQueue
|
||||||
@ -143,7 +159,7 @@ func preHandleMetadata(metadata *C.Metadata) error {
|
|||||||
metadata.DNSMode = C.DNSFakeIP
|
metadata.DNSMode = C.DNSFakeIP
|
||||||
} else if node := resolver.DefaultHosts.Search(host); node != nil {
|
} else if node := resolver.DefaultHosts.Search(host); node != nil {
|
||||||
// redir-host should lookup the hosts
|
// redir-host should lookup the hosts
|
||||||
metadata.DstIP = node.Data.(net.IP)
|
metadata.DstIP = node.Data.AsSlice()
|
||||||
}
|
}
|
||||||
} else if resolver.IsFakeIP(metadata.DstIP) {
|
} else if resolver.IsFakeIP(metadata.DstIP) {
|
||||||
return fmt.Errorf("fake DNS record %s missing", metadata.DstIP)
|
return fmt.Errorf("fake DNS record %s missing", metadata.DstIP)
|
||||||
@ -286,14 +302,24 @@ func handleTCPConn(connCtx C.ConnContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
|
||||||
|
defer cancel()
|
||||||
|
if MitmOutbound != nil && metadata.Type != C.MITM {
|
||||||
|
if remoteConn, err1 := MitmOutbound.DialContext(ctx, metadata); err1 == nil {
|
||||||
|
remoteConn = statistic.NewSniffing(remoteConn, metadata)
|
||||||
|
defer remoteConn.Close()
|
||||||
|
|
||||||
|
handleSocket(connCtx, remoteConn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
proxy, rule, err := resolveMetadata(connCtx, metadata)
|
proxy, rule, err := resolveMetadata(connCtx, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnln("[Metadata] parse failed: %s", err.Error())
|
log.Warnln("[Metadata] parse failed: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
|
|
||||||
defer cancel()
|
|
||||||
remoteConn, err := proxy.DialContext(ctx, metadata.Pure())
|
remoteConn, err := proxy.DialContext(ctx, metadata.Pure())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if rule == nil {
|
if rule == nil {
|
||||||
@ -333,8 +359,7 @@ func match(metadata *C.Metadata) (C.Proxy, C.Rule, error) {
|
|||||||
var resolved bool
|
var resolved bool
|
||||||
|
|
||||||
if node := resolver.DefaultHosts.Search(metadata.Host); node != nil {
|
if node := resolver.DefaultHosts.Search(metadata.Host); node != nil {
|
||||||
ip := node.Data.(net.IP)
|
metadata.DstIP = node.Data.AsSlice()
|
||||||
metadata.DstIP = ip
|
|
||||||
resolved = true
|
resolved = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -388,8 +413,7 @@ func matchScript(metadata *C.Metadata) (C.Proxy, error) {
|
|||||||
defer configMux.RUnlock()
|
defer configMux.RUnlock()
|
||||||
|
|
||||||
if node := resolver.DefaultHosts.Search(metadata.Host); node != nil {
|
if node := resolver.DefaultHosts.Search(metadata.Host); node != nil {
|
||||||
ip := node.Data.(net.IP)
|
metadata.DstIP = node.Data.AsSlice()
|
||||||
metadata.DstIP = ip
|
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter, err := S.CallPyMainFunction(metadata)
|
adapter, err := S.CallPyMainFunction(metadata)
|
||||||
|
Reference in New Issue
Block a user