Compare commits
62 Commits
Author | SHA1 | Date | |
---|---|---|---|
d1fb442bd5 | |||
7c4a359a2b | |||
0cdc40beb3 | |||
4cd8b6f24f | |||
0f63682bdf | |||
52125a3992 | |||
06c9dfdb80 | |||
54386ccda3 | |||
d3c50cf89f | |||
50d2e082d5 | |||
c38469330d | |||
045c3a3ad4 | |||
904c354ee4 | |||
1a8a6d0b5d | |||
e0c8aed5c7 | |||
8f60d61ff9 | |||
5e6ab99403 | |||
8adcc4d83b | |||
b76737bdbb | |||
09f435d928 | |||
3dd9ea52d8 | |||
09917a2a95 | |||
96a4abf46c | |||
16e3090ee8 | |||
b3e10c05e6 | |||
112b3e5a6c | |||
60fdd82e2b | |||
9815010131 | |||
9875f8ea6e | |||
9e0bd62790 | |||
0d51877fcd | |||
e34090c39a | |||
5cc66e51db | |||
71f0a4e35e | |||
48a2013d9c | |||
6f3a654d6c | |||
0f7f0a9b1a | |||
d59e98dc83 | |||
b137a50d85 | |||
f75cd04181 | |||
b926f4cf09 | |||
5829c3d5be | |||
288afd1308 | |||
528fbd10e4 | |||
85128a634d | |||
f6acbaac7b | |||
b75da2c6d8 | |||
271ed2b9c1 | |||
1702e7ddb4 | |||
1fd8f690fe | |||
183e776970 | |||
f00dfdd34d | |||
0670275533 | |||
9e77c650d9 | |||
3497fdaf45 | |||
6077e825c5 | |||
0dd2a6dee5 | |||
c1b5e4f561 | |||
1a21c8ebfd | |||
f867f02546 | |||
7c6c147a18 | |||
0eff8516c0 |
21
.github/workflows/go.yml
vendored
Normal file
21
.github/workflows/go.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
name: Go
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.13
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.13
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go test ./...
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -14,3 +14,9 @@ bin/*
|
||||
|
||||
# dep
|
||||
vendor
|
||||
|
||||
# GoLand
|
||||
.idea/*
|
||||
|
||||
# macOS file
|
||||
.DS_Store
|
||||
|
@ -1,7 +1,7 @@
|
||||
language: go
|
||||
sudo: false
|
||||
go:
|
||||
- '1.12'
|
||||
- '1.13'
|
||||
install:
|
||||
- "go mod download"
|
||||
env:
|
||||
|
13
Dockerfile
13
Dockerfile
@ -1,21 +1,18 @@
|
||||
FROM golang:latest as builder
|
||||
FROM golang:alpine as builder
|
||||
|
||||
RUN wget http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz -O /tmp/GeoLite2-Country.tar.gz && \
|
||||
RUN apk add --no-cache make git && \
|
||||
wget http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz -O /tmp/GeoLite2-Country.tar.gz && \
|
||||
tar zxvf /tmp/GeoLite2-Country.tar.gz -C /tmp && \
|
||||
mv /tmp/GeoLite2-Country_*/GeoLite2-Country.mmdb /Country.mmdb
|
||||
|
||||
WORKDIR /clash-src
|
||||
|
||||
COPY . /clash-src
|
||||
|
||||
RUN go mod download && \
|
||||
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' -o /clash
|
||||
make linux-amd64 && \
|
||||
mv ./bin/clash-linux-amd64 /clash
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk add --no-cache ca-certificates
|
||||
|
||||
COPY --from=builder /Country.mmdb /root/.config/clash/
|
||||
COPY --from=builder /clash /
|
||||
|
||||
ENTRYPOINT ["/clash"]
|
||||
|
10
Makefile
10
Makefile
@ -16,7 +16,8 @@ PLATFORM_LIST = \
|
||||
linux-armv8 \
|
||||
linux-mips-softfloat \
|
||||
linux-mips-hardfloat \
|
||||
linux-mipsle \
|
||||
linux-mipsle-softfloat \
|
||||
linux-mipsle-hardfloat \
|
||||
linux-mips64 \
|
||||
linux-mips64le \
|
||||
freebsd-386 \
|
||||
@ -55,8 +56,11 @@ linux-mips-softfloat:
|
||||
linux-mips-hardfloat:
|
||||
GOARCH=mips GOMIPS=hardfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-mipsle:
|
||||
GOARCH=mipsle GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
linux-mipsle-softfloat:
|
||||
GOARCH=mipsle GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-mipsle-hardfloat:
|
||||
GOARCH=mipsle GOMIPS=hardfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-mips64:
|
||||
GOARCH=mips64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
182
README.md
182
README.md
@ -20,40 +20,39 @@
|
||||
|
||||
## Features
|
||||
|
||||
- HTTP/HTTPS and SOCKS protocol
|
||||
- Surge like configuration
|
||||
- Local HTTP/HTTPS/SOCKS server
|
||||
- Surge-like configuration format
|
||||
- GeoIP rule support
|
||||
- Support Vmess/Shadowsocks/Socks5
|
||||
- Support for Netfilter TCP redirect
|
||||
- Supports Vmess, Shadowsocks, Snell and SOCKS5 protocol
|
||||
- Supports Netfilter TCP redirecting
|
||||
- Comprehensive HTTP API
|
||||
|
||||
## Install
|
||||
|
||||
You can build from source:
|
||||
Clash Requires Go >= 1.13. You can build it from source:
|
||||
|
||||
```sh
|
||||
go get -u -v github.com/Dreamacro/clash
|
||||
$ go get -u -v github.com/Dreamacro/clash
|
||||
```
|
||||
|
||||
Pre-built binaries are available: [release](https://github.com/Dreamacro/clash/releases)
|
||||
Pre-built binaries are available here: [release](https://github.com/Dreamacro/clash/releases)
|
||||
|
||||
Requires Go >= 1.12.
|
||||
|
||||
Checkout Clash version:
|
||||
Check Clash version with:
|
||||
|
||||
```sh
|
||||
clash -v
|
||||
$ clash -v
|
||||
```
|
||||
|
||||
## Daemon
|
||||
|
||||
Unfortunately, there is no native elegant way to implement golang's daemon.
|
||||
Unfortunately, there is no native and elegant way to implement daemons on Golang.
|
||||
|
||||
So we can use third-party daemon tools like pm2, supervisor, and so on.
|
||||
So we can use third-party daemon tools like PM2, Supervisor or the like.
|
||||
|
||||
In the case of [pm2](https://github.com/Unitech/pm2), we can start the daemon this way:
|
||||
|
||||
```sh
|
||||
pm2 start clash
|
||||
$ pm2 start clash
|
||||
```
|
||||
|
||||
If you have Docker installed, you can run clash directly using `docker-compose`.
|
||||
@ -62,19 +61,20 @@ If you have Docker installed, you can run clash directly using `docker-compose`.
|
||||
|
||||
## Config
|
||||
|
||||
The default configuration directory is `$HOME/.config/clash`
|
||||
The default configuration directory is `$HOME/.config/clash`.
|
||||
|
||||
The name of the configuration file is `config.yaml`
|
||||
The name of the configuration file is `config.yaml`.
|
||||
|
||||
If you want to use another directory, you can use `-d` to control the configuration directory
|
||||
If you want to use another directory, use `-d` to control the configuration directory.
|
||||
|
||||
For example, you can use the current directory as the configuration directory
|
||||
For example, you can use the current directory as the configuration directory:
|
||||
|
||||
```sh
|
||||
clash -d .
|
||||
$ clash -d .
|
||||
```
|
||||
|
||||
Below is a simple demo configuration file:
|
||||
<details>
|
||||
<summary>This is an example configuration file</summary>
|
||||
|
||||
```yml
|
||||
# port of HTTP
|
||||
@ -88,6 +88,12 @@ socks-port: 7891
|
||||
|
||||
allow-lan: false
|
||||
|
||||
# Only applicable when setting allow-lan to true
|
||||
# "*": bind all IP addresses
|
||||
# 192.168.122.11: bind a single IPv4 address
|
||||
# "[aaaa::a8aa:ff:fe09:57d8]": bind a single IPv6 address
|
||||
# bind-address: "*"
|
||||
|
||||
# Rule / Global/ Direct (default is Rule)
|
||||
mode: Rule
|
||||
|
||||
@ -95,7 +101,7 @@ mode: Rule
|
||||
# info / warning / error / debug / silent
|
||||
log-level: info
|
||||
|
||||
# A RESTful API for clash
|
||||
# RESTful API for clash
|
||||
external-controller: 127.0.0.1:9090
|
||||
|
||||
# you can put the static web resource (such as clash-dashboard) to a directory, and clash would serve in `${API}/ui`
|
||||
@ -114,6 +120,12 @@ experimental:
|
||||
# - "user1:pass1"
|
||||
# - "user2:pass2"
|
||||
|
||||
# # experimental hosts, support wildcard (e.g. *.clash.dev Even *.foo.*.example.com)
|
||||
# # static domain has a higher priority than wildcard domain (foo.example.com > *.example.com)
|
||||
# hosts:
|
||||
# '*.clash.dev': 127.0.0.1
|
||||
# 'alpha.clash.dev': '::1'
|
||||
|
||||
# dns:
|
||||
# enable: true # set true to enable dns (default is false)
|
||||
# ipv6: false # default is false
|
||||
@ -126,21 +138,34 @@ experimental:
|
||||
# - https://1.1.1.1/dns-query # dns over https
|
||||
# fallback: # concurrent request with nameserver, fallback used when GEOIP country isn't CN
|
||||
# - tcp://1.1.1.1
|
||||
# fallback-filter:
|
||||
# geoip: true # default
|
||||
# ipcidr: # ips in these subnets will be considered polluted
|
||||
# - 240.0.0.0/4
|
||||
|
||||
Proxy:
|
||||
|
||||
# shadowsocks
|
||||
# The types of cipher are consistent with go-shadowsocks2
|
||||
# support AEAD_AES_128_GCM AEAD_AES_192_GCM AEAD_AES_256_GCM AEAD_CHACHA20_POLY1305 AES-128-CTR AES-192-CTR AES-256-CTR AES-128-CFB AES-192-CFB AES-256-CFB CHACHA20-IETF XCHACHA20
|
||||
# In addition to what go-shadowsocks2 supports, it also supports chacha20 rc4-md5 xchacha20-ietf-poly1305
|
||||
- { name: "ss1", type: ss, server: server, port: 443, cipher: AEAD_CHACHA20_POLY1305, password: "password", udp: true }
|
||||
# The supported ciphers(encrypt methods):
|
||||
# aes-128-gcm aes-192-gcm aes-256-gcm
|
||||
# aes-128-cfb aes-192-cfb aes-256-cfb
|
||||
# aes-128-ctr aes-192-ctr aes-256-ctr
|
||||
# rc4-md5 chacha20 chacha20-ietf xchacha20
|
||||
# chacha20-ietf-poly1305 xchacha20-ietf-poly1305
|
||||
- name: "ss1"
|
||||
type: ss
|
||||
server: server
|
||||
port: 443
|
||||
cipher: chacha20-ietf-poly1305
|
||||
password: "password"
|
||||
# udp: true
|
||||
|
||||
# old obfs configuration remove after prerelease
|
||||
# old obfs configuration format remove after prerelease
|
||||
- name: "ss2"
|
||||
type: ss
|
||||
server: server
|
||||
port: 443
|
||||
cipher: AEAD_CHACHA20_POLY1305
|
||||
cipher: chacha20-ietf-poly1305
|
||||
password: "password"
|
||||
plugin: obfs
|
||||
plugin-opts:
|
||||
@ -151,7 +176,7 @@ Proxy:
|
||||
type: ss
|
||||
server: server
|
||||
port: 443
|
||||
cipher: AEAD_CHACHA20_POLY1305
|
||||
cipher: chacha20-ietf-poly1305
|
||||
password: "password"
|
||||
plugin: v2ray-plugin
|
||||
plugin-opts:
|
||||
@ -160,52 +185,98 @@ Proxy:
|
||||
# skip-cert-verify: true
|
||||
# host: bing.com
|
||||
# path: "/"
|
||||
# mux: true
|
||||
# headers:
|
||||
# custom: value
|
||||
|
||||
# vmess
|
||||
# cipher support auto/aes-128-gcm/chacha20-poly1305/none
|
||||
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto }
|
||||
# with tls
|
||||
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, tls: true }
|
||||
# with tls and skip-cert-verify
|
||||
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, tls: true, skip-cert-verify: true }
|
||||
# with ws-path and ws-headers
|
||||
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, network: ws, ws-path: /path, ws-headers: { Host: v2ray.com } }
|
||||
# with ws + tls
|
||||
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, network: ws, ws-path: /path, tls: true }
|
||||
- name: "vmess"
|
||||
type: vmess
|
||||
server: server
|
||||
port: 443
|
||||
uuid: uuid
|
||||
alterId: 32
|
||||
cipher: auto
|
||||
# udp: true
|
||||
# tls: true
|
||||
# skip-cert-verify: true
|
||||
# network: ws
|
||||
# ws-path: /path
|
||||
# ws-headers:
|
||||
# Host: v2ray.com
|
||||
|
||||
# socks5
|
||||
- { name: "socks", type: socks5, server: server, port: 443 }
|
||||
# socks5 with authentication
|
||||
- { name: "socks", type: socks5, server: server, port: 443, username: "username", password: "password" }
|
||||
# with tls
|
||||
- { name: "socks", type: socks5, server: server, port: 443, tls: true }
|
||||
# with tls and skip-cert-verify
|
||||
- { name: "socks", type: socks5, server: server, port: 443, tls: true, skip-cert-verify: true }
|
||||
- name: "socks"
|
||||
type: socks5
|
||||
server: server
|
||||
port: 443
|
||||
# username: username
|
||||
# password: password
|
||||
# tls: true
|
||||
# skip-cert-verify: true
|
||||
# udp: true
|
||||
|
||||
# http
|
||||
- { name: "http", type: http, server: server, port: 443 }
|
||||
# http with authentication
|
||||
- { name: "http", type: http, server: server, port: 443, username: "username", password: "password" }
|
||||
# with tls (https)
|
||||
- { name: "http", type: http, server: server, port: 443, tls: true }
|
||||
# with tls (https) and skip-cert-verify
|
||||
- { name: "http", type: http, server: server, port: 443, tls: true, skip-cert-verify: true }
|
||||
- name: "http"
|
||||
type: http
|
||||
server: server
|
||||
port: 443
|
||||
# username: username
|
||||
# password: password
|
||||
# tls: true # https
|
||||
# skip-cert-verify: true
|
||||
|
||||
# snell
|
||||
- name: "snell"
|
||||
type: snell
|
||||
server: server
|
||||
port: 44046
|
||||
psk: yourpsk
|
||||
# obfs-opts:
|
||||
# mode: http # or tls
|
||||
# host: bing.com
|
||||
|
||||
Proxy Group:
|
||||
# url-test select which proxy will be used by benchmarking speed to a URL.
|
||||
- { name: "auto", type: url-test, proxies: ["ss1", "ss2", "vmess1"], url: "http://www.gstatic.com/generate_204", interval: 300 }
|
||||
- name: "auto"
|
||||
type: url-test
|
||||
proxies:
|
||||
- ss1
|
||||
- ss2
|
||||
- vmess1
|
||||
url: 'http://www.gstatic.com/generate_204'
|
||||
interval: 300
|
||||
|
||||
# fallback select an available policy by priority. The availability is tested by accessing an URL, just like an auto url-test group.
|
||||
- { name: "fallback-auto", type: fallback, proxies: ["ss1", "ss2", "vmess1"], url: "http://www.gstatic.com/generate_204", interval: 300 }
|
||||
- name: "fallback-auto"
|
||||
type: fallback
|
||||
proxies:
|
||||
- ss1
|
||||
- ss2
|
||||
- vmess1
|
||||
url: 'http://www.gstatic.com/generate_204'
|
||||
interval: 300
|
||||
|
||||
# load-balance: The request of the same eTLD will be dial on the same proxy.
|
||||
- { name: "load-balance", type: load-balance, proxies: ["ss1", "ss2", "vmess1"], url: "http://www.gstatic.com/generate_204", interval: 300 }
|
||||
- name: "load-balance"
|
||||
type: load-balance
|
||||
proxies:
|
||||
- ss1
|
||||
- ss2
|
||||
- vmess1
|
||||
url: 'http://www.gstatic.com/generate_204'
|
||||
interval: 300
|
||||
|
||||
# select is used for selecting proxy or proxy group
|
||||
# you can use RESTful API to switch proxy, is recommended for use in GUI.
|
||||
- { name: "Proxy", type: select, proxies: ["ss1", "ss2", "vmess1", "auto"] }
|
||||
- name: Proxy
|
||||
type: select
|
||||
proxies:
|
||||
- ss1
|
||||
- ss2
|
||||
- vmess1
|
||||
- auto
|
||||
|
||||
Rule:
|
||||
- DOMAIN-SUFFIX,google.com,auto
|
||||
@ -222,6 +293,7 @@ Rule:
|
||||
# you also can use `FINAL,Proxy` or `FINAL,,Proxy` now
|
||||
- MATCH,auto
|
||||
```
|
||||
</details>
|
||||
|
||||
## Documentations
|
||||
https://clash.gitbook.io/
|
||||
|
@ -1,6 +1,7 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
@ -11,6 +12,10 @@ import (
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultURLTestTimeout = time.Second * 5
|
||||
)
|
||||
|
||||
type Base struct {
|
||||
name string
|
||||
tp C.AdapterType
|
||||
@ -25,7 +30,7 @@ func (b *Base) Type() C.AdapterType {
|
||||
return b.tp
|
||||
}
|
||||
|
||||
func (b *Base) DialUDP(metadata *C.Metadata) (net.PacketConn, net.Addr, error) {
|
||||
func (b *Base) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
|
||||
return nil, nil, errors.New("no support")
|
||||
}
|
||||
|
||||
@ -41,6 +46,40 @@ func (b *Base) MarshalJSON() ([]byte, error) {
|
||||
})
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
net.Conn
|
||||
chain C.Chain
|
||||
}
|
||||
|
||||
func (c *conn) Chains() C.Chain {
|
||||
return c.chain
|
||||
}
|
||||
|
||||
func (c *conn) AppendToChains(a C.ProxyAdapter) {
|
||||
c.chain = append(c.chain, a.Name())
|
||||
}
|
||||
|
||||
func newConn(c net.Conn, a C.ProxyAdapter) C.Conn {
|
||||
return &conn{c, []string{a.Name()}}
|
||||
}
|
||||
|
||||
type packetConn struct {
|
||||
net.PacketConn
|
||||
chain C.Chain
|
||||
}
|
||||
|
||||
func (c *packetConn) Chains() C.Chain {
|
||||
return c.chain
|
||||
}
|
||||
|
||||
func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
|
||||
c.chain = append(c.chain, a.Name())
|
||||
}
|
||||
|
||||
func newPacketConn(c net.PacketConn, a C.ProxyAdapter) C.PacketConn {
|
||||
return &packetConn{c, []string{a.Name()}}
|
||||
}
|
||||
|
||||
type Proxy struct {
|
||||
C.ProxyAdapter
|
||||
history *queue.Queue
|
||||
@ -51,8 +90,14 @@ func (p *Proxy) Alive() bool {
|
||||
return p.alive
|
||||
}
|
||||
|
||||
func (p *Proxy) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
conn, err := p.ProxyAdapter.Dial(metadata)
|
||||
func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
|
||||
defer cancel()
|
||||
return p.DialContext(ctx, metadata)
|
||||
}
|
||||
|
||||
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
conn, err := p.ProxyAdapter.DialContext(ctx, metadata)
|
||||
if err != nil {
|
||||
p.alive = false
|
||||
}
|
||||
@ -68,18 +113,18 @@ func (p *Proxy) DelayHistory() []C.DelayHistory {
|
||||
return histories
|
||||
}
|
||||
|
||||
// LastDelay return last history record. if proxy is not alive, return the max value of int16.
|
||||
// LastDelay return last history record. if proxy is not alive, return the max value of uint16.
|
||||
func (p *Proxy) LastDelay() (delay uint16) {
|
||||
var max uint16 = 0xffff
|
||||
if !p.alive {
|
||||
return max
|
||||
}
|
||||
|
||||
head := p.history.First()
|
||||
if head == nil {
|
||||
last := p.history.Last()
|
||||
if last == nil {
|
||||
return max
|
||||
}
|
||||
history := head.(C.DelayHistory)
|
||||
history := last.(C.DelayHistory)
|
||||
if history.Delay == 0 {
|
||||
return max
|
||||
}
|
||||
@ -99,7 +144,7 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
|
||||
// URLTest get the delay for the specified URL
|
||||
func (p *Proxy) URLTest(url string) (t uint16, err error) {
|
||||
func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
|
||||
defer func() {
|
||||
p.alive = err == nil
|
||||
record := C.DelayHistory{Time: time.Now()}
|
||||
@ -118,11 +163,18 @@ func (p *Proxy) URLTest(url string) (t uint16, err error) {
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
instance, err := p.Dial(&addr)
|
||||
instance, err := p.DialContext(ctx, &addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer instance.Close()
|
||||
|
||||
req, err := http.NewRequest(http.MethodHead, url, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
transport := &http.Transport{
|
||||
Dial: func(string, string) (net.Conn, error) {
|
||||
return instance, nil
|
||||
@ -133,8 +185,9 @@ func (p *Proxy) URLTest(url string) (t uint16, err error) {
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
|
||||
client := http.Client{Transport: transport}
|
||||
resp, err := client.Get(url)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -146,3 +199,9 @@ func (p *Proxy) URLTest(url string) (t uint16, err error) {
|
||||
func NewProxy(adapter C.ProxyAdapter) *Proxy {
|
||||
return &Proxy{adapter, queue.New(10), true}
|
||||
}
|
||||
|
||||
// ProxyGroupOption contain the common options for all kind of ProxyGroup
|
||||
type ProxyGroupOption struct {
|
||||
Name string `proxy:"name"`
|
||||
Proxies []string `proxy:"proxies"`
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
@ -10,31 +11,31 @@ type Direct struct {
|
||||
*Base
|
||||
}
|
||||
|
||||
func (d *Direct) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
address := net.JoinHostPort(metadata.Host, metadata.DstPort)
|
||||
if metadata.DstIP != nil {
|
||||
address = net.JoinHostPort(metadata.DstIP.String(), metadata.DstPort)
|
||||
}
|
||||
|
||||
c, err := dialTimeout("tcp", address, tcpTimeout)
|
||||
c, err := dialContext(ctx, "tcp", address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
return c, nil
|
||||
return newConn(c, d), nil
|
||||
}
|
||||
|
||||
func (d *Direct) DialUDP(metadata *C.Metadata) (net.PacketConn, net.Addr, error) {
|
||||
func (d *Direct) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
|
||||
pc, err := net.ListenPacket("udp", "")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
addr, err := resolveUDPAddr("udp", net.JoinHostPort(metadata.String(), metadata.DstPort))
|
||||
addr, err := resolveUDPAddr("udp", metadata.RemoteAddress())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return pc, addr, nil
|
||||
return newPacketConn(pc, d), addr, nil
|
||||
}
|
||||
|
||||
func NewDirect() *Direct {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
@ -30,14 +31,22 @@ func (f *Fallback) Now() string {
|
||||
return proxy.Name()
|
||||
}
|
||||
|
||||
func (f *Fallback) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
proxy := f.findAliveProxy()
|
||||
return proxy.Dial(metadata)
|
||||
c, err := proxy.DialContext(ctx, metadata)
|
||||
if err == nil {
|
||||
c.AppendToChains(f)
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
func (f *Fallback) DialUDP(metadata *C.Metadata) (net.PacketConn, net.Addr, error) {
|
||||
func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
|
||||
proxy := f.findAliveProxy()
|
||||
return proxy.DialUDP(metadata)
|
||||
pc, addr, err := proxy.DialUDP(metadata)
|
||||
if err == nil {
|
||||
pc.AppendToChains(f)
|
||||
}
|
||||
return pc, addr, err
|
||||
}
|
||||
|
||||
func (f *Fallback) SupportUDP() bool {
|
||||
@ -90,7 +99,7 @@ func (f *Fallback) validTest() {
|
||||
|
||||
for _, p := range f.proxies {
|
||||
go func(p C.Proxy) {
|
||||
p.URLTest(f.rawURL)
|
||||
p.URLTest(context.Background(), f.rawURL)
|
||||
wg.Done()
|
||||
}(p)
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package adapters
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
@ -35,8 +36,8 @@ type HttpOption struct {
|
||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||
}
|
||||
|
||||
func (h *Http) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
c, err := dialTimeout("tcp", h.addr, tcpTimeout)
|
||||
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
c, err := dialContext(ctx, "tcp", h.addr)
|
||||
if err == nil && h.tls {
|
||||
cc := tls.Client(c, h.tlsConfig)
|
||||
err = cc.Handshake()
|
||||
@ -51,14 +52,14 @@ func (h *Http) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
return newConn(c, h), nil
|
||||
}
|
||||
|
||||
func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
|
||||
var buf bytes.Buffer
|
||||
var err error
|
||||
|
||||
addr := net.JoinHostPort(metadata.String(), metadata.DstPort)
|
||||
addr := metadata.RemoteAddress()
|
||||
buf.WriteString("CONNECT " + addr + " HTTP/1.1\r\n")
|
||||
buf.WriteString("Host: " + metadata.String() + "\r\n")
|
||||
buf.WriteString("Proxy-Connection: Keep-Alive\r\n")
|
||||
@ -81,17 +82,21 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
if resp.StatusCode == 407 {
|
||||
if resp.StatusCode == http.StatusProxyAuthRequired {
|
||||
return errors.New("HTTP need auth")
|
||||
}
|
||||
|
||||
if resp.StatusCode == 405 {
|
||||
if resp.StatusCode == http.StatusMethodNotAllowed {
|
||||
return errors.New("CONNECT method not allowed by proxy")
|
||||
}
|
||||
|
||||
if resp.StatusCode >= http.StatusInternalServerError {
|
||||
return errors.New(resp.Status)
|
||||
}
|
||||
return fmt.Errorf("can not connect remote err code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
@ -53,21 +54,34 @@ func jumpHash(key uint64, buckets int32) int32 {
|
||||
return int32(b)
|
||||
}
|
||||
|
||||
func (lb *LoadBalance) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
|
||||
defer func() {
|
||||
if err == nil {
|
||||
c.AppendToChains(lb)
|
||||
}
|
||||
}()
|
||||
|
||||
key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
|
||||
buckets := int32(len(lb.proxies))
|
||||
for i := 0; i < lb.maxRetry; i, key = i+1, key+1 {
|
||||
idx := jumpHash(key, buckets)
|
||||
proxy := lb.proxies[idx]
|
||||
if proxy.Alive() {
|
||||
return proxy.Dial(metadata)
|
||||
c, err = proxy.DialContext(ctx, metadata)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return lb.proxies[0].Dial(metadata)
|
||||
c, err = lb.proxies[0].DialContext(ctx, metadata)
|
||||
return
|
||||
}
|
||||
|
||||
func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (net.PacketConn, net.Addr, error) {
|
||||
func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (pc C.PacketConn, addr net.Addr, err error) {
|
||||
defer func() {
|
||||
if err == nil {
|
||||
pc.AppendToChains(lb)
|
||||
}
|
||||
}()
|
||||
|
||||
key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
|
||||
buckets := int32(len(lb.proxies))
|
||||
for i := 0; i < lb.maxRetry; i, key = i+1, key+1 {
|
||||
@ -95,7 +109,7 @@ func (lb *LoadBalance) validTest() {
|
||||
|
||||
for _, p := range lb.proxies {
|
||||
go func(p C.Proxy) {
|
||||
p.URLTest(lb.rawURL)
|
||||
p.URLTest(context.Background(), lb.rawURL)
|
||||
wg.Done()
|
||||
}(p)
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
@ -12,8 +13,8 @@ type Reject struct {
|
||||
*Base
|
||||
}
|
||||
|
||||
func (r *Reject) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
return &NopConn{}, nil
|
||||
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
return newConn(&NopConn{}, r), nil
|
||||
}
|
||||
|
||||
func NewReject() *Reject {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
@ -20,12 +21,20 @@ type SelectorOption struct {
|
||||
Proxies []string `proxy:"proxies"`
|
||||
}
|
||||
|
||||
func (s *Selector) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
return s.selected.Dial(metadata)
|
||||
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
c, err := s.selected.DialContext(ctx, metadata)
|
||||
if err == nil {
|
||||
c.AppendToChains(s)
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
func (s *Selector) DialUDP(metadata *C.Metadata) (net.PacketConn, net.Addr, error) {
|
||||
return s.selected.DialUDP(metadata)
|
||||
func (s *Selector) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
|
||||
pc, addr, err := s.selected.DialUDP(metadata)
|
||||
if err == nil {
|
||||
pc.AppendToChains(s)
|
||||
}
|
||||
return pc, addr, err
|
||||
}
|
||||
|
||||
func (s *Selector) SupportUDP() bool {
|
||||
|
@ -1,13 +1,13 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/Dreamacro/clash/common/pool"
|
||||
"github.com/Dreamacro/clash/common/structure"
|
||||
obfs "github.com/Dreamacro/clash/component/simple-obfs"
|
||||
"github.com/Dreamacro/clash/component/socks5"
|
||||
@ -23,9 +23,9 @@ type ShadowSocks struct {
|
||||
cipher core.Cipher
|
||||
|
||||
// obfs
|
||||
obfsMode string
|
||||
obfsOption *simpleObfsOption
|
||||
wsOption *v2rayObfs.WebsocketOption
|
||||
obfsMode string
|
||||
obfsOption *simpleObfsOption
|
||||
v2rayOption *v2rayObfs.Option
|
||||
}
|
||||
|
||||
type ShadowSocksOption struct {
|
||||
@ -55,10 +55,11 @@ type v2rayObfsOption struct {
|
||||
TLS bool `obfs:"tls,omitempty"`
|
||||
Headers map[string]string `obfs:"headers,omitempty"`
|
||||
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
|
||||
Mux bool `obfs:"mux,omitempty"`
|
||||
}
|
||||
|
||||
func (ss *ShadowSocks) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
c, err := dialTimeout("tcp", ss.server, tcpTimeout)
|
||||
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
c, err := dialContext(ctx, "tcp", ss.server)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %s", ss.server, err.Error())
|
||||
}
|
||||
@ -71,17 +72,17 @@ func (ss *ShadowSocks) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
c = obfs.NewHTTPObfs(c, ss.obfsOption.Host, port)
|
||||
case "websocket":
|
||||
var err error
|
||||
c, err = v2rayObfs.NewWebsocketObfs(c, ss.wsOption)
|
||||
c, err = v2rayObfs.NewV2rayObfs(c, ss.v2rayOption)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %s", ss.server, err.Error())
|
||||
}
|
||||
}
|
||||
c = ss.cipher.StreamConn(c)
|
||||
_, err = c.Write(serializesSocksAddr(metadata))
|
||||
return c, err
|
||||
return newConn(c, ss), err
|
||||
}
|
||||
|
||||
func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (net.PacketConn, net.Addr, error) {
|
||||
func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
|
||||
pc, err := net.ListenPacket("udp", "")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@ -92,13 +93,13 @@ func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (net.PacketConn, net.Addr,
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
remoteAddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(metadata.String(), metadata.DstPort))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
targetAddr := socks5.ParseAddr(metadata.RemoteAddress())
|
||||
if targetAddr == nil {
|
||||
return nil, nil, fmt.Errorf("parse address error: %v:%v", metadata.String(), metadata.DstPort)
|
||||
}
|
||||
|
||||
pc = ss.cipher.PacketConn(pc)
|
||||
return &ssUDPConn{PacketConn: pc, rAddr: remoteAddr}, addr, nil
|
||||
return newPacketConn(&ssUDPConn{PacketConn: pc, rAddr: targetAddr}, ss), addr, nil
|
||||
}
|
||||
|
||||
func (ss *ShadowSocks) MarshalJSON() ([]byte, error) {
|
||||
@ -116,7 +117,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||
return nil, fmt.Errorf("ss %s initialize error: %s", server, err.Error())
|
||||
}
|
||||
|
||||
var wsOption *v2rayObfs.WebsocketOption
|
||||
var v2rayOption *v2rayObfs.Option
|
||||
var obfsOption *simpleObfsOption
|
||||
obfsMode := ""
|
||||
|
||||
@ -144,7 +145,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||
obfsMode = opts.Mode
|
||||
obfsOption = &opts
|
||||
} else if option.Plugin == "v2ray-plugin" {
|
||||
opts := v2rayObfsOption{Host: "bing.com"}
|
||||
opts := v2rayObfsOption{Host: "bing.com", Mux: true}
|
||||
if err := decoder.Decode(option.PluginOpts, &opts); err != nil {
|
||||
return nil, fmt.Errorf("ss %s initialize v2ray-plugin error: %s", server, err.Error())
|
||||
}
|
||||
@ -162,11 +163,12 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||
ClientSessionCache: getClientSessionCache(),
|
||||
}
|
||||
}
|
||||
wsOption = &v2rayObfs.WebsocketOption{
|
||||
v2rayOption = &v2rayObfs.Option{
|
||||
Host: opts.Host,
|
||||
Path: opts.Path,
|
||||
Headers: opts.Headers,
|
||||
TLSConfig: tlsConfig,
|
||||
Mux: opts.Mux,
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,24 +181,23 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||
server: server,
|
||||
cipher: ciph,
|
||||
|
||||
obfsMode: obfsMode,
|
||||
wsOption: wsOption,
|
||||
obfsOption: obfsOption,
|
||||
obfsMode: obfsMode,
|
||||
v2rayOption: v2rayOption,
|
||||
obfsOption: obfsOption,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type ssUDPConn struct {
|
||||
net.PacketConn
|
||||
rAddr net.Addr
|
||||
rAddr socks5.Addr
|
||||
}
|
||||
|
||||
func (uc *ssUDPConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
||||
buf := pool.BufPool.Get().([]byte)
|
||||
defer pool.BufPool.Put(buf[:cap(buf)])
|
||||
rAddr := socks5.ParseAddr(uc.rAddr.String())
|
||||
copy(buf[len(rAddr):], b)
|
||||
copy(buf, rAddr)
|
||||
return uc.PacketConn.WriteTo(buf[:len(rAddr)+len(b)], addr)
|
||||
func (uc *ssUDPConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
|
||||
packet, err := socks5.EncodeUDPPacket(uc.rAddr, b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return uc.PacketConn.WriteTo(packet[3:], addr)
|
||||
}
|
||||
|
||||
func (uc *ssUDPConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||
|
72
adapters/outbound/snell.go
Normal file
72
adapters/outbound/snell.go
Normal file
@ -0,0 +1,72 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/Dreamacro/clash/common/structure"
|
||||
obfs "github.com/Dreamacro/clash/component/simple-obfs"
|
||||
"github.com/Dreamacro/clash/component/snell"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type Snell struct {
|
||||
*Base
|
||||
server string
|
||||
psk []byte
|
||||
obfsOption *simpleObfsOption
|
||||
}
|
||||
|
||||
type SnellOption struct {
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
Psk string `proxy:"psk"`
|
||||
ObfsOpts map[string]interface{} `proxy:"obfs-opts,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
c, err := dialContext(ctx, "tcp", s.server)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %s", s.server, err.Error())
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
switch s.obfsOption.Mode {
|
||||
case "tls":
|
||||
c = obfs.NewTLSObfs(c, s.obfsOption.Host)
|
||||
case "http":
|
||||
_, port, _ := net.SplitHostPort(s.server)
|
||||
c = obfs.NewHTTPObfs(c, s.obfsOption.Host, port)
|
||||
}
|
||||
c = snell.StreamConn(c, s.psk)
|
||||
port, _ := strconv.Atoi(metadata.DstPort)
|
||||
err = snell.WriteHeader(c, metadata.String(), uint(port))
|
||||
return newConn(c, s), err
|
||||
}
|
||||
|
||||
func NewSnell(option SnellOption) (*Snell, error) {
|
||||
server := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||
psk := []byte(option.Psk)
|
||||
|
||||
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
|
||||
obfsOption := &simpleObfsOption{Host: "bing.com"}
|
||||
if err := decoder.Decode(option.ObfsOpts, obfsOption); err != nil {
|
||||
return nil, fmt.Errorf("snell %s initialize obfs error: %s", server, err.Error())
|
||||
}
|
||||
|
||||
if obfsOption.Mode != "tls" && obfsOption.Mode != "http" {
|
||||
return nil, fmt.Errorf("snell %s obfs mode error: %s", server, obfsOption.Mode)
|
||||
}
|
||||
|
||||
return &Snell{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
tp: C.Snell,
|
||||
},
|
||||
server: server,
|
||||
psk: psk,
|
||||
obfsOption: obfsOption,
|
||||
}, nil
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
@ -31,8 +34,8 @@ type Socks5Option struct {
|
||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||
}
|
||||
|
||||
func (ss *Socks5) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
c, err := dialTimeout("tcp", ss.addr, tcpTimeout)
|
||||
func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
c, err := dialContext(ctx, "tcp", ss.addr)
|
||||
|
||||
if err == nil && ss.tls {
|
||||
cc := tls.Client(c, ss.tlsConfig)
|
||||
@ -51,24 +54,33 @@ func (ss *Socks5) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
Password: ss.pass,
|
||||
}
|
||||
}
|
||||
if err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdConnect, user); err != nil {
|
||||
if _, err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdConnect, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
return newConn(c, ss), nil
|
||||
}
|
||||
|
||||
func (ss *Socks5) DialUDP(metadata *C.Metadata) (net.PacketConn, net.Addr, error) {
|
||||
c, err := dialTimeout("tcp", ss.addr, tcpTimeout)
|
||||
func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, _ net.Addr, err error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
|
||||
defer cancel()
|
||||
c, err := dialContext(ctx, "tcp", ss.addr)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%s connect error", ss.addr)
|
||||
return
|
||||
}
|
||||
|
||||
if err == nil && ss.tls {
|
||||
if ss.tls {
|
||||
cc := tls.Client(c, ss.tlsConfig)
|
||||
err = cc.Handshake()
|
||||
c = cc
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("%s connect error", ss.addr)
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
c.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
tcpKeepAlive(c)
|
||||
var user *socks5.User
|
||||
if ss.user != "" {
|
||||
@ -78,10 +90,36 @@ func (ss *Socks5) DialUDP(metadata *C.Metadata) (net.PacketConn, net.Addr, error
|
||||
}
|
||||
}
|
||||
|
||||
if err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdUDPAssociate, user); err != nil {
|
||||
return nil, nil, err
|
||||
bindAddr, err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdUDPAssociate, user)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%v client hanshake error", err)
|
||||
return
|
||||
}
|
||||
return &fakeUDPConn{Conn: c}, c.LocalAddr(), nil
|
||||
|
||||
addr, err := net.ResolveUDPAddr("udp", bindAddr.String())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
targetAddr := socks5.ParseAddr(metadata.RemoteAddress())
|
||||
if targetAddr == nil {
|
||||
return nil, nil, fmt.Errorf("parse address error: %v:%v", metadata.String(), metadata.DstPort)
|
||||
}
|
||||
|
||||
pc, err := net.ListenPacket("udp", "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
io.Copy(ioutil.Discard, c)
|
||||
c.Close()
|
||||
// A UDP association terminates when the TCP connection that the UDP
|
||||
// ASSOCIATE request arrived on terminates. RFC1928
|
||||
pc.Close()
|
||||
}()
|
||||
|
||||
return newPacketConn(&socksUDPConn{PacketConn: pc, rAddr: targetAddr, tcpConn: c}, ss), addr, nil
|
||||
}
|
||||
|
||||
func NewSocks5(option Socks5Option) *Socks5 {
|
||||
@ -108,3 +146,37 @@ func NewSocks5(option Socks5Option) *Socks5 {
|
||||
tlsConfig: tlsConfig,
|
||||
}
|
||||
}
|
||||
|
||||
type socksUDPConn struct {
|
||||
net.PacketConn
|
||||
rAddr socks5.Addr
|
||||
tcpConn net.Conn
|
||||
}
|
||||
|
||||
func (uc *socksUDPConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
|
||||
packet, err := socks5.EncodeUDPPacket(uc.rAddr, b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return uc.PacketConn.WriteTo(packet, addr)
|
||||
}
|
||||
|
||||
func (uc *socksUDPConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||
n, a, e := uc.PacketConn.ReadFrom(b)
|
||||
if e != nil {
|
||||
return 0, nil, e
|
||||
}
|
||||
addr, payload, err := socks5.DecodeUDPPacket(b)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
// due to DecodeUDPPacket is mutable, record addr length
|
||||
addrLength := len(addr)
|
||||
copy(b, payload)
|
||||
return n - addrLength - 3, a, nil
|
||||
}
|
||||
|
||||
func (uc *socksUDPConn) Close() error {
|
||||
uc.tcpConn.Close()
|
||||
return uc.PacketConn.Close()
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@ -34,16 +33,24 @@ func (u *URLTest) Now() string {
|
||||
return u.fast.Name()
|
||||
}
|
||||
|
||||
func (u *URLTest) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
a, err := u.fast.Dial(metadata)
|
||||
if err != nil {
|
||||
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
|
||||
for i := 0; i < 3; i++ {
|
||||
c, err = u.fast.DialContext(ctx, metadata)
|
||||
if err == nil {
|
||||
c.AppendToChains(u)
|
||||
return
|
||||
}
|
||||
u.fallback()
|
||||
}
|
||||
return a, err
|
||||
return
|
||||
}
|
||||
|
||||
func (u *URLTest) DialUDP(metadata *C.Metadata) (net.PacketConn, net.Addr, error) {
|
||||
return u.fast.DialUDP(metadata)
|
||||
func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
|
||||
pc, addr, err := u.fast.DialUDP(metadata)
|
||||
if err == nil {
|
||||
pc.AppendToChains(u)
|
||||
}
|
||||
return pc, addr, err
|
||||
}
|
||||
|
||||
func (u *URLTest) SupportUDP() bool {
|
||||
@ -103,36 +110,26 @@ func (u *URLTest) speedTest() {
|
||||
}
|
||||
defer atomic.StoreInt32(&u.once, 0)
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(u.proxies))
|
||||
c := make(chan interface{})
|
||||
fast := picker.SelectFast(context.Background(), c)
|
||||
timer := time.NewTimer(u.interval)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
|
||||
defer cancel()
|
||||
picker := picker.WithoutAutoCancel(ctx)
|
||||
for _, p := range u.proxies {
|
||||
go func(p C.Proxy) {
|
||||
_, err := p.URLTest(u.rawURL)
|
||||
if err == nil {
|
||||
c <- p
|
||||
proxy := p
|
||||
picker.Go(func() (interface{}, error) {
|
||||
_, err := proxy.URLTest(ctx, u.rawURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wg.Done()
|
||||
}(p)
|
||||
return proxy, nil
|
||||
})
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(c)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-timer.C:
|
||||
// Wait for fast to return or close.
|
||||
<-fast
|
||||
case p, open := <-fast:
|
||||
if open {
|
||||
u.fast = p.(C.Proxy)
|
||||
}
|
||||
fast := picker.WaitWithoutCancel()
|
||||
if fast != nil {
|
||||
u.fast = fast.(C.Proxy)
|
||||
}
|
||||
|
||||
picker.Wait()
|
||||
}
|
||||
|
||||
func NewURLTest(option URLTestOption, proxies []C.Proxy) (*URLTest, error) {
|
||||
|
@ -2,6 +2,7 @@ package adapters
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
@ -85,31 +86,84 @@ func serializesSocksAddr(metadata *C.Metadata) []byte {
|
||||
return bytes.Join(buf, nil)
|
||||
}
|
||||
|
||||
type fakeUDPConn struct {
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (fuc *fakeUDPConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
||||
return fuc.Conn.Write(b)
|
||||
}
|
||||
|
||||
func (fuc *fakeUDPConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||
n, err := fuc.Conn.Read(b)
|
||||
return n, fuc.RemoteAddr(), err
|
||||
}
|
||||
|
||||
func dialTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
|
||||
func dialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ip, err := dns.ResolveIP(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
dialer := net.Dialer{}
|
||||
|
||||
returned := make(chan struct{})
|
||||
defer close(returned)
|
||||
|
||||
type dialResult struct {
|
||||
net.Conn
|
||||
error
|
||||
resolved bool
|
||||
ipv6 bool
|
||||
done bool
|
||||
}
|
||||
results := make(chan dialResult)
|
||||
var primary, fallback dialResult
|
||||
|
||||
startRacer := func(ctx context.Context, host string, ipv6 bool) {
|
||||
result := dialResult{ipv6: ipv6, done: true}
|
||||
defer func() {
|
||||
select {
|
||||
case results <- result:
|
||||
case <-returned:
|
||||
if result.Conn != nil {
|
||||
result.Conn.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var ip net.IP
|
||||
if ipv6 {
|
||||
ip, result.error = dns.ResolveIPv6(host)
|
||||
} else {
|
||||
ip, result.error = dns.ResolveIPv4(host)
|
||||
}
|
||||
if result.error != nil {
|
||||
return
|
||||
}
|
||||
result.resolved = true
|
||||
|
||||
if ipv6 {
|
||||
result.Conn, result.error = dialer.DialContext(ctx, "tcp6", net.JoinHostPort(ip.String(), port))
|
||||
} else {
|
||||
result.Conn, result.error = dialer.DialContext(ctx, "tcp4", net.JoinHostPort(ip.String(), port))
|
||||
}
|
||||
}
|
||||
|
||||
return net.DialTimeout(network, net.JoinHostPort(ip.String(), port), timeout)
|
||||
go startRacer(ctx, host, false)
|
||||
go startRacer(ctx, host, true)
|
||||
|
||||
for {
|
||||
select {
|
||||
case res := <-results:
|
||||
if res.error == nil {
|
||||
return res.Conn, nil
|
||||
}
|
||||
|
||||
if !res.ipv6 {
|
||||
primary = res
|
||||
} else {
|
||||
fallback = res
|
||||
}
|
||||
|
||||
if primary.done && fallback.done {
|
||||
if primary.resolved {
|
||||
return nil, primary.error
|
||||
} else if fallback.resolved {
|
||||
return nil, fallback.error
|
||||
} else {
|
||||
return nil, primary.error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resolveUDPAddr(network, address string) (*net.UDPAddr, error) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
@ -31,24 +32,29 @@ type VmessOption struct {
|
||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||
}
|
||||
|
||||
func (v *Vmess) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
c, err := dialTimeout("tcp", v.server, tcpTimeout)
|
||||
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
c, err := dialContext(ctx, "tcp", v.server)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error", v.server)
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
c, err = v.client.New(c, parseVmessAddr(metadata))
|
||||
return c, err
|
||||
return newConn(c, v), err
|
||||
}
|
||||
|
||||
func (v *Vmess) DialUDP(metadata *C.Metadata) (net.PacketConn, net.Addr, error) {
|
||||
c, err := dialTimeout("tcp", v.server, tcpTimeout)
|
||||
func (v *Vmess) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
|
||||
defer cancel()
|
||||
c, err := dialContext(ctx, "tcp", v.server)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("%s connect error", v.server)
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
c, err = v.client.New(c, parseVmessAddr(metadata))
|
||||
return &fakeUDPConn{Conn: c}, c.LocalAddr(), err
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("new vmess client error: %v", err)
|
||||
}
|
||||
return newPacketConn(&vmessUDPConn{Conn: c}, v), c.RemoteAddr(), nil
|
||||
}
|
||||
|
||||
func NewVmess(option VmessOption) (*Vmess, error) {
|
||||
@ -64,7 +70,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
|
||||
WebSocketPath: option.WSPath,
|
||||
WebSocketHeaders: option.WSHeaders,
|
||||
SkipCertVerify: option.SkipCertVerify,
|
||||
SessionCacahe: getClientSessionCache(),
|
||||
SessionCache: getClientSessionCache(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -74,7 +80,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
tp: C.Vmess,
|
||||
udp: option.UDP,
|
||||
udp: true,
|
||||
},
|
||||
server: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||
client: client,
|
||||
@ -108,3 +114,16 @@ func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
|
||||
Port: uint(port),
|
||||
}
|
||||
}
|
||||
|
||||
type vmessUDPConn struct {
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (uc *vmessUDPConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
||||
return uc.Conn.Write(b)
|
||||
}
|
||||
|
||||
func (uc *vmessUDPConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||
n, err := uc.Conn.Read(b)
|
||||
return n, uc.RemoteAddr(), err
|
||||
}
|
||||
|
30
common/cache/cache_test.go
vendored
30
common/cache/cache_test.go
vendored
@ -4,6 +4,8 @@ import (
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCache_Basic(t *testing.T) {
|
||||
@ -14,32 +16,30 @@ func TestCache_Basic(t *testing.T) {
|
||||
c.Put("string", "a", ttl)
|
||||
|
||||
i := c.Get("int")
|
||||
if i.(int) != 1 {
|
||||
t.Error("should recv 1")
|
||||
}
|
||||
assert.Equal(t, i.(int), 1, "should recv 1")
|
||||
|
||||
s := c.Get("string")
|
||||
if s.(string) != "a" {
|
||||
t.Error("should recv 'a'")
|
||||
}
|
||||
assert.Equal(t, s.(string), "a", "should recv 'a'")
|
||||
}
|
||||
|
||||
func TestCache_TTL(t *testing.T) {
|
||||
interval := 200 * time.Millisecond
|
||||
ttl := 20 * time.Millisecond
|
||||
now := time.Now()
|
||||
c := New(interval)
|
||||
c.Put("int", 1, ttl)
|
||||
c.Put("int2", 2, ttl)
|
||||
|
||||
i := c.Get("int")
|
||||
if i.(int) != 1 {
|
||||
t.Error("should recv 1")
|
||||
}
|
||||
_, expired := c.GetWithExpire("int2")
|
||||
assert.Equal(t, i.(int), 1, "should recv 1")
|
||||
assert.True(t, now.Before(expired))
|
||||
|
||||
time.Sleep(ttl * 2)
|
||||
i = c.Get("int")
|
||||
if i != nil {
|
||||
t.Error("should recv nil")
|
||||
}
|
||||
j, _ := c.GetWithExpire("int2")
|
||||
assert.Nil(t, i, "should recv nil")
|
||||
assert.Nil(t, j, "should recv nil")
|
||||
}
|
||||
|
||||
func TestCache_AutoCleanup(t *testing.T) {
|
||||
@ -50,9 +50,9 @@ func TestCache_AutoCleanup(t *testing.T) {
|
||||
|
||||
time.Sleep(ttl * 2)
|
||||
i := c.Get("int")
|
||||
if i != nil {
|
||||
t.Error("should recv nil")
|
||||
}
|
||||
j, _ := c.GetWithExpire("int")
|
||||
assert.Nil(t, i, "should recv nil")
|
||||
assert.Nil(t, j, "should recv nil")
|
||||
}
|
||||
|
||||
func TestCache_AutoGC(t *testing.T) {
|
||||
|
157
common/cache/lrucache.go
vendored
Normal file
157
common/cache/lrucache.go
vendored
Normal file
@ -0,0 +1,157 @@
|
||||
package cache
|
||||
|
||||
// Modified by https://github.com/die-net/lrucache
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Option is part of Functional Options Pattern
|
||||
type Option func(*LruCache)
|
||||
|
||||
// WithUpdateAgeOnGet update expires when Get element
|
||||
func WithUpdateAgeOnGet() Option {
|
||||
return func(l *LruCache) {
|
||||
l.updateAgeOnGet = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithAge defined element max age (second)
|
||||
func WithAge(maxAge int64) Option {
|
||||
return func(l *LruCache) {
|
||||
l.maxAge = maxAge
|
||||
}
|
||||
}
|
||||
|
||||
// WithSize defined max length of LruCache
|
||||
func WithSize(maxSize int) Option {
|
||||
return func(l *LruCache) {
|
||||
l.maxSize = maxSize
|
||||
}
|
||||
}
|
||||
|
||||
// LruCache is a thread-safe, in-memory lru-cache that evicts the
|
||||
// least recently used entries from memory when (if set) the entries are
|
||||
// older than maxAge (in seconds). Use the New constructor to create one.
|
||||
type LruCache struct {
|
||||
maxAge int64
|
||||
maxSize int
|
||||
mu sync.Mutex
|
||||
cache map[interface{}]*list.Element
|
||||
lru *list.List // Front is least-recent
|
||||
updateAgeOnGet bool
|
||||
}
|
||||
|
||||
// NewLRUCache creates an LruCache
|
||||
func NewLRUCache(options ...Option) *LruCache {
|
||||
lc := &LruCache{
|
||||
lru: list.New(),
|
||||
cache: make(map[interface{}]*list.Element),
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
option(lc)
|
||||
}
|
||||
|
||||
return lc
|
||||
}
|
||||
|
||||
// Get returns the interface{} representation of a cached response and a bool
|
||||
// set to true if the key was found.
|
||||
func (c *LruCache) Get(key interface{}) (interface{}, bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
le, ok := c.cache[key]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if c.maxAge > 0 && le.Value.(*entry).expires <= time.Now().Unix() {
|
||||
c.deleteElement(le)
|
||||
c.maybeDeleteOldest()
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
c.lru.MoveToBack(le)
|
||||
entry := le.Value.(*entry)
|
||||
if c.maxAge > 0 && c.updateAgeOnGet {
|
||||
entry.expires = time.Now().Unix() + c.maxAge
|
||||
}
|
||||
value := entry.value
|
||||
|
||||
return value, true
|
||||
}
|
||||
|
||||
// Exist returns if key exist in cache but not put item to the head of linked list
|
||||
func (c *LruCache) Exist(key interface{}) bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
_, ok := c.cache[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Set stores the interface{} representation of a response for a given key.
|
||||
func (c *LruCache) Set(key interface{}, value interface{}) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
expires := int64(0)
|
||||
if c.maxAge > 0 {
|
||||
expires = time.Now().Unix() + c.maxAge
|
||||
}
|
||||
|
||||
if le, ok := c.cache[key]; ok {
|
||||
c.lru.MoveToBack(le)
|
||||
e := le.Value.(*entry)
|
||||
e.value = value
|
||||
e.expires = expires
|
||||
} else {
|
||||
e := &entry{key: key, value: value, expires: expires}
|
||||
c.cache[key] = c.lru.PushBack(e)
|
||||
|
||||
if c.maxSize > 0 {
|
||||
if len := c.lru.Len(); len > c.maxSize {
|
||||
c.deleteElement(c.lru.Front())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.maybeDeleteOldest()
|
||||
}
|
||||
|
||||
// Delete removes the value associated with a key.
|
||||
func (c *LruCache) Delete(key string) {
|
||||
c.mu.Lock()
|
||||
|
||||
if le, ok := c.cache[key]; ok {
|
||||
c.deleteElement(le)
|
||||
}
|
||||
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *LruCache) maybeDeleteOldest() {
|
||||
if c.maxAge > 0 {
|
||||
now := time.Now().Unix()
|
||||
for le := c.lru.Front(); le != nil && le.Value.(*entry).expires <= now; le = c.lru.Front() {
|
||||
c.deleteElement(le)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *LruCache) deleteElement(le *list.Element) {
|
||||
c.lru.Remove(le)
|
||||
e := le.Value.(*entry)
|
||||
delete(c.cache, e.key)
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
key interface{}
|
||||
value interface{}
|
||||
expires int64
|
||||
}
|
117
common/cache/lrucache_test.go
vendored
Normal file
117
common/cache/lrucache_test.go
vendored
Normal file
@ -0,0 +1,117 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var entries = []struct {
|
||||
key string
|
||||
value string
|
||||
}{
|
||||
{"1", "one"},
|
||||
{"2", "two"},
|
||||
{"3", "three"},
|
||||
{"4", "four"},
|
||||
{"5", "five"},
|
||||
}
|
||||
|
||||
func TestLRUCache(t *testing.T) {
|
||||
c := NewLRUCache()
|
||||
|
||||
for _, e := range entries {
|
||||
c.Set(e.key, e.value)
|
||||
}
|
||||
|
||||
c.Delete("missing")
|
||||
_, ok := c.Get("missing")
|
||||
assert.False(t, ok)
|
||||
|
||||
for _, e := range entries {
|
||||
value, ok := c.Get(e.key)
|
||||
if assert.True(t, ok) {
|
||||
assert.Equal(t, e.value, value.(string))
|
||||
}
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
c.Delete(e.key)
|
||||
|
||||
_, ok := c.Get(e.key)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLRUMaxAge(t *testing.T) {
|
||||
c := NewLRUCache(WithAge(86400))
|
||||
|
||||
now := time.Now().Unix()
|
||||
expected := now + 86400
|
||||
|
||||
// Add one expired entry
|
||||
c.Set("foo", "bar")
|
||||
c.lru.Back().Value.(*entry).expires = now
|
||||
|
||||
// Reset
|
||||
c.Set("foo", "bar")
|
||||
e := c.lru.Back().Value.(*entry)
|
||||
assert.True(t, e.expires >= now)
|
||||
c.lru.Back().Value.(*entry).expires = now
|
||||
|
||||
// Set a few and verify expiration times
|
||||
for _, s := range entries {
|
||||
c.Set(s.key, s.value)
|
||||
e := c.lru.Back().Value.(*entry)
|
||||
assert.True(t, e.expires >= expected && e.expires <= expected+10)
|
||||
}
|
||||
|
||||
// Make sure we can get them all
|
||||
for _, s := range entries {
|
||||
_, ok := c.Get(s.key)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
// Expire all entries
|
||||
for _, s := range entries {
|
||||
le, ok := c.cache[s.key]
|
||||
if assert.True(t, ok) {
|
||||
le.Value.(*entry).expires = now
|
||||
}
|
||||
}
|
||||
|
||||
// Get one expired entry, which should clear all expired entries
|
||||
_, ok := c.Get("3")
|
||||
assert.False(t, ok)
|
||||
assert.Equal(t, c.lru.Len(), 0)
|
||||
}
|
||||
|
||||
func TestLRUpdateOnGet(t *testing.T) {
|
||||
c := NewLRUCache(WithAge(86400), WithUpdateAgeOnGet())
|
||||
|
||||
now := time.Now().Unix()
|
||||
expires := now + 86400/2
|
||||
|
||||
// Add one expired entry
|
||||
c.Set("foo", "bar")
|
||||
c.lru.Back().Value.(*entry).expires = expires
|
||||
|
||||
_, ok := c.Get("foo")
|
||||
assert.True(t, ok)
|
||||
assert.True(t, c.lru.Back().Value.(*entry).expires > expires)
|
||||
}
|
||||
|
||||
func TestMaxSize(t *testing.T) {
|
||||
c := NewLRUCache(WithSize(2))
|
||||
// Add one expired entry
|
||||
c.Set("foo", "bar")
|
||||
_, ok := c.Get("foo")
|
||||
assert.True(t, ok)
|
||||
|
||||
c.Set("bar", "foo")
|
||||
c.Set("baz", "foo")
|
||||
|
||||
_, ok = c.Get("foo")
|
||||
assert.False(t, ok)
|
||||
}
|
@ -1,22 +1,89 @@
|
||||
package picker
|
||||
|
||||
import "context"
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Picker provides synchronization, and Context cancelation
|
||||
// for groups of goroutines working on subtasks of a common task.
|
||||
// Inspired by errGroup
|
||||
type Picker struct {
|
||||
ctx context.Context
|
||||
cancel func()
|
||||
|
||||
wg sync.WaitGroup
|
||||
|
||||
once sync.Once
|
||||
result interface{}
|
||||
|
||||
firstDone chan struct{}
|
||||
}
|
||||
|
||||
func newPicker(ctx context.Context, cancel func()) *Picker {
|
||||
return &Picker{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
firstDone: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
|
||||
// WithContext returns a new Picker and an associated Context derived from ctx.
|
||||
// and cancel when first element return.
|
||||
func WithContext(ctx context.Context) (*Picker, context.Context) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
return newPicker(ctx, cancel), ctx
|
||||
}
|
||||
|
||||
// WithTimeout returns a new Picker and an associated Context derived from ctx with timeout.
|
||||
func WithTimeout(ctx context.Context, timeout time.Duration) (*Picker, context.Context) {
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
return newPicker(ctx, cancel), ctx
|
||||
}
|
||||
|
||||
// WithoutAutoCancel returns a new Picker and an associated Context derived from ctx,
|
||||
// but it wouldn't cancel context when the first element return.
|
||||
func WithoutAutoCancel(ctx context.Context) *Picker {
|
||||
return newPicker(ctx, nil)
|
||||
}
|
||||
|
||||
// Wait blocks until all function calls from the Go method have returned,
|
||||
// then returns the first nil error result (if any) from them.
|
||||
func (p *Picker) Wait() interface{} {
|
||||
p.wg.Wait()
|
||||
if p.cancel != nil {
|
||||
p.cancel()
|
||||
}
|
||||
return p.result
|
||||
}
|
||||
|
||||
// WaitWithoutCancel blocks until the first result return, if timeout will return nil.
|
||||
func (p *Picker) WaitWithoutCancel() interface{} {
|
||||
select {
|
||||
case <-p.firstDone:
|
||||
return p.result
|
||||
case <-p.ctx.Done():
|
||||
return p.result
|
||||
}
|
||||
}
|
||||
|
||||
// Go calls the given function in a new goroutine.
|
||||
// The first call to return a nil error cancels the group; its result will be returned by Wait.
|
||||
func (p *Picker) Go(f func() (interface{}, error)) {
|
||||
p.wg.Add(1)
|
||||
|
||||
func SelectFast(ctx context.Context, in <-chan interface{}) <-chan interface{} {
|
||||
out := make(chan interface{})
|
||||
go func() {
|
||||
select {
|
||||
case p, open := <-in:
|
||||
if open {
|
||||
out <- p
|
||||
}
|
||||
case <-ctx.Done():
|
||||
}
|
||||
defer p.wg.Done()
|
||||
|
||||
close(out)
|
||||
for range in {
|
||||
if ret, err := f(); err == nil {
|
||||
p.once.Do(func() {
|
||||
p.result = ret
|
||||
p.firstDone <- struct{}{}
|
||||
if p.cancel != nil {
|
||||
p.cancel()
|
||||
}
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
return out
|
||||
}
|
||||
|
@ -4,41 +4,63 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func sleepAndSend(delay int, in chan<- interface{}, input interface{}) {
|
||||
time.Sleep(time.Millisecond * time.Duration(delay))
|
||||
in <- input
|
||||
}
|
||||
|
||||
func sleepAndClose(delay int, in chan interface{}) {
|
||||
time.Sleep(time.Millisecond * time.Duration(delay))
|
||||
close(in)
|
||||
func sleepAndSend(ctx context.Context, delay int, input interface{}) func() (interface{}, error) {
|
||||
return func() (interface{}, error) {
|
||||
timer := time.NewTimer(time.Millisecond * time.Duration(delay))
|
||||
select {
|
||||
case <-timer.C:
|
||||
return input, nil
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPicker_Basic(t *testing.T) {
|
||||
in := make(chan interface{})
|
||||
fast := SelectFast(context.Background(), in)
|
||||
go sleepAndSend(20, in, 1)
|
||||
go sleepAndSend(30, in, 2)
|
||||
go sleepAndClose(40, in)
|
||||
picker, ctx := WithContext(context.Background())
|
||||
picker.Go(sleepAndSend(ctx, 30, 2))
|
||||
picker.Go(sleepAndSend(ctx, 20, 1))
|
||||
|
||||
number, exist := <-fast
|
||||
if !exist || number != 1 {
|
||||
t.Error("should recv 1", exist, number)
|
||||
}
|
||||
number := picker.Wait()
|
||||
assert.NotNil(t, number)
|
||||
assert.Equal(t, number.(int), 1)
|
||||
}
|
||||
|
||||
func TestPicker_Timeout(t *testing.T) {
|
||||
in := make(chan interface{})
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*5)
|
||||
defer cancel()
|
||||
fast := SelectFast(ctx, in)
|
||||
go sleepAndSend(20, in, 1)
|
||||
go sleepAndClose(30, in)
|
||||
picker, ctx := WithTimeout(context.Background(), time.Millisecond*5)
|
||||
picker.Go(sleepAndSend(ctx, 20, 1))
|
||||
|
||||
_, exist := <-fast
|
||||
if exist {
|
||||
t.Error("should recv false")
|
||||
}
|
||||
number := picker.Wait()
|
||||
assert.Nil(t, number)
|
||||
}
|
||||
|
||||
func TestPicker_WaitWithoutAutoCancel(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*60)
|
||||
defer cancel()
|
||||
picker := WithoutAutoCancel(ctx)
|
||||
|
||||
trigger := false
|
||||
picker.Go(sleepAndSend(ctx, 10, 1))
|
||||
picker.Go(func() (interface{}, error) {
|
||||
timer := time.NewTimer(time.Millisecond * time.Duration(30))
|
||||
select {
|
||||
case <-timer.C:
|
||||
trigger = true
|
||||
return 2, nil
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
})
|
||||
elm := picker.WaitWithoutCancel()
|
||||
|
||||
assert.NotNil(t, elm)
|
||||
assert.Equal(t, elm.(int), 1)
|
||||
|
||||
elm = picker.Wait()
|
||||
assert.True(t, trigger)
|
||||
assert.Equal(t, elm.(int), 1)
|
||||
}
|
||||
|
@ -34,16 +34,16 @@ func (q *Queue) Pop() interface{} {
|
||||
return head
|
||||
}
|
||||
|
||||
// First returns the head of items without deleting.
|
||||
func (q *Queue) First() interface{} {
|
||||
// Last returns the last of item.
|
||||
func (q *Queue) Last() interface{} {
|
||||
if len(q.items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
q.lock.RLock()
|
||||
head := q.items[0]
|
||||
last := q.items[len(q.items)-1]
|
||||
q.lock.RUnlock()
|
||||
return head
|
||||
return last
|
||||
}
|
||||
|
||||
// Copy get the copy of queue.
|
||||
|
26
component/domain-trie/node.go
Normal file
26
component/domain-trie/node.go
Normal file
@ -0,0 +1,26 @@
|
||||
package trie
|
||||
|
||||
// Node is the trie's node
|
||||
type Node struct {
|
||||
Data interface{}
|
||||
children map[string]*Node
|
||||
}
|
||||
|
||||
func (n *Node) getChild(s string) *Node {
|
||||
return n.children[s]
|
||||
}
|
||||
|
||||
func (n *Node) hasChild(s string) bool {
|
||||
return n.getChild(s) != nil
|
||||
}
|
||||
|
||||
func (n *Node) addChild(s string, child *Node) {
|
||||
n.children[s] = child
|
||||
}
|
||||
|
||||
func newNode(data interface{}) *Node {
|
||||
return &Node{
|
||||
Data: data,
|
||||
children: map[string]*Node{},
|
||||
}
|
||||
}
|
92
component/domain-trie/tire.go
Normal file
92
component/domain-trie/tire.go
Normal file
@ -0,0 +1,92 @@
|
||||
package trie
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
wildcard = "*"
|
||||
domainStep = "."
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidDomain means insert domain is invalid
|
||||
ErrInvalidDomain = errors.New("invalid domain")
|
||||
)
|
||||
|
||||
// Trie contains the main logic for adding and searching nodes for domain segments.
|
||||
// support wildcard domain (e.g *.google.com)
|
||||
type Trie struct {
|
||||
root *Node
|
||||
}
|
||||
|
||||
func isValidDomain(domain string) bool {
|
||||
return domain != "" && domain[0] != '.' && domain[len(domain)-1] != '.'
|
||||
}
|
||||
|
||||
// Insert adds a node to the trie.
|
||||
// Support
|
||||
// 1. www.example.com
|
||||
// 2. *.example.com
|
||||
// 3. subdomain.*.example.com
|
||||
func (t *Trie) Insert(domain string, data interface{}) error {
|
||||
if !isValidDomain(domain) {
|
||||
return ErrInvalidDomain
|
||||
}
|
||||
|
||||
parts := strings.Split(domain, domainStep)
|
||||
node := t.root
|
||||
// reverse storage domain part to save space
|
||||
for i := len(parts) - 1; i >= 0; i-- {
|
||||
part := parts[i]
|
||||
if !node.hasChild(part) {
|
||||
node.addChild(part, newNode(nil))
|
||||
}
|
||||
|
||||
node = node.getChild(part)
|
||||
}
|
||||
|
||||
node.Data = data
|
||||
return nil
|
||||
}
|
||||
|
||||
// Search is the most important part of the Trie.
|
||||
// Priority as:
|
||||
// 1. static part
|
||||
// 2. wildcard domain
|
||||
func (t *Trie) Search(domain string) *Node {
|
||||
if !isValidDomain(domain) {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(domain, domainStep)
|
||||
|
||||
n := t.root
|
||||
for i := len(parts) - 1; i >= 0; i-- {
|
||||
part := parts[i]
|
||||
|
||||
var child *Node
|
||||
if !n.hasChild(part) {
|
||||
if !n.hasChild(wildcard) {
|
||||
return nil
|
||||
}
|
||||
|
||||
child = n.getChild(wildcard)
|
||||
} else {
|
||||
child = n.getChild(part)
|
||||
}
|
||||
|
||||
n = child
|
||||
}
|
||||
|
||||
if n.Data == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// New returns a new, empty Trie.
|
||||
func New() *Trie {
|
||||
return &Trie{root: newNode(nil)}
|
||||
}
|
87
component/domain-trie/trie_test.go
Normal file
87
component/domain-trie/trie_test.go
Normal file
@ -0,0 +1,87 @@
|
||||
package trie
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var localIP = net.IP{127, 0, 0, 1}
|
||||
|
||||
func TestTrie_Basic(t *testing.T) {
|
||||
tree := New()
|
||||
domains := []string{
|
||||
"example.com",
|
||||
"google.com",
|
||||
}
|
||||
|
||||
for _, domain := range domains {
|
||||
tree.Insert(domain, localIP)
|
||||
}
|
||||
|
||||
node := tree.Search("example.com")
|
||||
if node == nil {
|
||||
t.Error("should not recv nil")
|
||||
}
|
||||
|
||||
if !node.Data.(net.IP).Equal(localIP) {
|
||||
t.Error("should equal 127.0.0.1")
|
||||
}
|
||||
|
||||
if tree.Insert("", localIP) == nil {
|
||||
t.Error("should return error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrie_Wildcard(t *testing.T) {
|
||||
tree := New()
|
||||
domains := []string{
|
||||
"*.example.com",
|
||||
"sub.*.example.com",
|
||||
"*.dev",
|
||||
}
|
||||
|
||||
for _, domain := range domains {
|
||||
tree.Insert(domain, localIP)
|
||||
}
|
||||
|
||||
if tree.Search("sub.example.com") == nil {
|
||||
t.Error("should not recv nil")
|
||||
}
|
||||
|
||||
if tree.Search("sub.foo.example.com") == nil {
|
||||
t.Error("should not recv nil")
|
||||
}
|
||||
|
||||
if tree.Search("foo.sub.example.com") != nil {
|
||||
t.Error("should recv nil")
|
||||
}
|
||||
|
||||
if tree.Search("foo.example.dev") != nil {
|
||||
t.Error("should recv nil")
|
||||
}
|
||||
|
||||
if tree.Search("example.com") != nil {
|
||||
t.Error("should recv nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrie_Boundary(t *testing.T) {
|
||||
tree := New()
|
||||
tree.Insert("*.dev", localIP)
|
||||
|
||||
if err := tree.Insert(".", localIP); err == nil {
|
||||
t.Error("should recv err")
|
||||
}
|
||||
|
||||
if err := tree.Insert(".com", localIP); err == nil {
|
||||
t.Error("should recv err")
|
||||
}
|
||||
|
||||
if tree.Search("dev") != nil {
|
||||
t.Error("should recv nil")
|
||||
}
|
||||
|
||||
if tree.Search(".dev") != nil {
|
||||
t.Error("should recv nil")
|
||||
}
|
||||
}
|
@ -4,22 +4,82 @@ import (
|
||||
"errors"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/Dreamacro/clash/common/cache"
|
||||
)
|
||||
|
||||
// Pool is a implementation about fake ip generator without storage
|
||||
type Pool struct {
|
||||
max uint32
|
||||
min uint32
|
||||
offset uint32
|
||||
mux *sync.Mutex
|
||||
max uint32
|
||||
min uint32
|
||||
gateway uint32
|
||||
offset uint32
|
||||
mux *sync.Mutex
|
||||
cache *cache.LruCache
|
||||
}
|
||||
|
||||
// Get return a new fake ip
|
||||
func (p *Pool) Get() net.IP {
|
||||
// Lookup return a fake ip with host
|
||||
func (p *Pool) Lookup(host string) net.IP {
|
||||
p.mux.Lock()
|
||||
defer p.mux.Unlock()
|
||||
ip := uintToIP(p.min + p.offset)
|
||||
p.offset = (p.offset + 1) % (p.max - p.min)
|
||||
if elm, exist := p.cache.Get(host); exist {
|
||||
ip := elm.(net.IP)
|
||||
|
||||
// ensure ip --> host on head of linked list
|
||||
n := ipToUint(ip.To4())
|
||||
offset := n - p.min + 1
|
||||
p.cache.Get(offset)
|
||||
return ip
|
||||
}
|
||||
|
||||
ip := p.get(host)
|
||||
p.cache.Set(host, ip)
|
||||
return ip
|
||||
}
|
||||
|
||||
// LookBack return host with the fake ip
|
||||
func (p *Pool) LookBack(ip net.IP) (string, bool) {
|
||||
p.mux.Lock()
|
||||
defer p.mux.Unlock()
|
||||
|
||||
if ip = ip.To4(); ip == nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
n := ipToUint(ip.To4())
|
||||
offset := n - p.min + 1
|
||||
|
||||
if elm, exist := p.cache.Get(offset); exist {
|
||||
host := elm.(string)
|
||||
|
||||
// ensure host --> ip on head of linked list
|
||||
p.cache.Get(host)
|
||||
return host, true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Gateway return gateway ip
|
||||
func (p *Pool) Gateway() net.IP {
|
||||
return uintToIP(p.gateway)
|
||||
}
|
||||
|
||||
func (p *Pool) get(host string) net.IP {
|
||||
current := p.offset
|
||||
for {
|
||||
p.offset = (p.offset + 1) % (p.max - p.min)
|
||||
// Avoid infinite loops
|
||||
if p.offset == current {
|
||||
break
|
||||
}
|
||||
|
||||
if !p.cache.Exist(p.offset) {
|
||||
break
|
||||
}
|
||||
}
|
||||
ip := uintToIP(p.min + p.offset - 1)
|
||||
p.cache.Set(p.offset, host)
|
||||
return ip
|
||||
}
|
||||
|
||||
@ -36,8 +96,8 @@ func uintToIP(v uint32) net.IP {
|
||||
}
|
||||
|
||||
// New return Pool instance
|
||||
func New(ipnet *net.IPNet) (*Pool, error) {
|
||||
min := ipToUint(ipnet.IP) + 1
|
||||
func New(ipnet *net.IPNet, size int) (*Pool, error) {
|
||||
min := ipToUint(ipnet.IP) + 2
|
||||
|
||||
ones, bits := ipnet.Mask.Size()
|
||||
total := 1<<uint(bits-ones) - 2
|
||||
@ -46,10 +106,12 @@ func New(ipnet *net.IPNet) (*Pool, error) {
|
||||
return nil, errors.New("ipnet don't have valid ip")
|
||||
}
|
||||
|
||||
max := min + uint32(total)
|
||||
max := min + uint32(total) - 1
|
||||
return &Pool{
|
||||
min: min,
|
||||
max: max,
|
||||
mux: &sync.Mutex{},
|
||||
min: min,
|
||||
max: max,
|
||||
gateway: min - 1,
|
||||
mux: &sync.Mutex{},
|
||||
cache: cache.NewLRUCache(cache.WithSize(size * 2)),
|
||||
}, nil
|
||||
}
|
||||
|
@ -3,42 +3,76 @@ package fakeip
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPool_Basic(t *testing.T) {
|
||||
_, ipnet, _ := net.ParseCIDR("192.168.0.1/30")
|
||||
pool, _ := New(ipnet)
|
||||
_, ipnet, _ := net.ParseCIDR("192.168.0.1/29")
|
||||
pool, _ := New(ipnet, 10)
|
||||
|
||||
first := pool.Get()
|
||||
last := pool.Get()
|
||||
first := pool.Lookup("foo.com")
|
||||
last := pool.Lookup("bar.com")
|
||||
bar, exist := pool.LookBack(last)
|
||||
|
||||
if !first.Equal(net.IP{192, 168, 0, 1}) {
|
||||
t.Error("should get right first ip, instead of", first.String())
|
||||
}
|
||||
|
||||
if !last.Equal(net.IP{192, 168, 0, 2}) {
|
||||
t.Error("should get right last ip, instead of", first.String())
|
||||
}
|
||||
assert.True(t, first.Equal(net.IP{192, 168, 0, 2}))
|
||||
assert.True(t, last.Equal(net.IP{192, 168, 0, 3}))
|
||||
assert.True(t, exist)
|
||||
assert.Equal(t, bar, "bar.com")
|
||||
}
|
||||
|
||||
func TestPool_Cycle(t *testing.T) {
|
||||
_, ipnet, _ := net.ParseCIDR("192.168.0.1/30")
|
||||
pool, _ := New(ipnet)
|
||||
pool, _ := New(ipnet, 10)
|
||||
|
||||
first := pool.Get()
|
||||
pool.Get()
|
||||
same := pool.Get()
|
||||
first := pool.Lookup("foo.com")
|
||||
same := pool.Lookup("baz.com")
|
||||
|
||||
if !first.Equal(same) {
|
||||
t.Error("should return same ip", first.String())
|
||||
}
|
||||
assert.True(t, first.Equal(same))
|
||||
}
|
||||
|
||||
func TestPool_MaxCacheSize(t *testing.T) {
|
||||
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
|
||||
pool, _ := New(ipnet, 2)
|
||||
|
||||
first := pool.Lookup("foo.com")
|
||||
pool.Lookup("bar.com")
|
||||
pool.Lookup("baz.com")
|
||||
next := pool.Lookup("foo.com")
|
||||
|
||||
assert.False(t, first.Equal(next))
|
||||
}
|
||||
|
||||
func TestPool_DoubleMapping(t *testing.T) {
|
||||
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
|
||||
pool, _ := New(ipnet, 2)
|
||||
|
||||
// fill cache
|
||||
fooIP := pool.Lookup("foo.com")
|
||||
bazIP := pool.Lookup("baz.com")
|
||||
|
||||
// make foo.com hot
|
||||
pool.Lookup("foo.com")
|
||||
|
||||
// should drop baz.com
|
||||
barIP := pool.Lookup("bar.com")
|
||||
|
||||
_, fooExist := pool.LookBack(fooIP)
|
||||
_, bazExist := pool.LookBack(bazIP)
|
||||
_, barExist := pool.LookBack(barIP)
|
||||
|
||||
newBazIP := pool.Lookup("baz.com")
|
||||
|
||||
assert.True(t, fooExist)
|
||||
assert.False(t, bazExist)
|
||||
assert.True(t, barExist)
|
||||
|
||||
assert.False(t, bazIP.Equal(newBazIP))
|
||||
}
|
||||
|
||||
func TestPool_Error(t *testing.T) {
|
||||
_, ipnet, _ := net.ParseCIDR("192.168.0.1/31")
|
||||
_, err := New(ipnet)
|
||||
_, err := New(ipnet, 10)
|
||||
|
||||
if err == nil {
|
||||
t.Error("should return err")
|
||||
}
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
46
component/nat/table.go
Normal file
46
component/nat/table.go
Normal file
@ -0,0 +1,46 @@
|
||||
package nat
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Table struct {
|
||||
mapping sync.Map
|
||||
}
|
||||
|
||||
type element struct {
|
||||
RemoteAddr net.Addr
|
||||
RemoteConn net.PacketConn
|
||||
}
|
||||
|
||||
func (t *Table) Set(key string, pc net.PacketConn, addr net.Addr) {
|
||||
// set conn read timeout
|
||||
t.mapping.Store(key, &element{
|
||||
RemoteConn: pc,
|
||||
RemoteAddr: addr,
|
||||
})
|
||||
}
|
||||
|
||||
func (t *Table) Get(key string) (net.PacketConn, net.Addr) {
|
||||
item, exist := t.mapping.Load(key)
|
||||
if !exist {
|
||||
return nil, nil
|
||||
}
|
||||
elm := item.(*element)
|
||||
return elm.RemoteConn, elm.RemoteAddr
|
||||
}
|
||||
|
||||
func (t *Table) GetOrCreateLock(key string) (*sync.WaitGroup, bool) {
|
||||
item, loaded := t.mapping.LoadOrStore(key, &sync.WaitGroup{})
|
||||
return item.(*sync.WaitGroup), loaded
|
||||
}
|
||||
|
||||
func (t *Table) Delete(key string) {
|
||||
t.mapping.Delete(key)
|
||||
}
|
||||
|
||||
// New return *Cache
|
||||
func New() *Table {
|
||||
return &Table{}
|
||||
}
|
21
component/snell/cipher.go
Normal file
21
component/snell/cipher.go
Normal file
@ -0,0 +1,21 @@
|
||||
package snell
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
)
|
||||
|
||||
type snellCipher struct {
|
||||
psk []byte
|
||||
makeAEAD func(key []byte) (cipher.AEAD, error)
|
||||
}
|
||||
|
||||
func (sc *snellCipher) KeySize() int { return 32 }
|
||||
func (sc *snellCipher) SaltSize() int { return 16 }
|
||||
func (sc *snellCipher) Encrypter(salt []byte) (cipher.AEAD, error) {
|
||||
return sc.makeAEAD(argon2.IDKey(sc.psk, salt, 3, 8, 1, uint32(sc.KeySize())))
|
||||
}
|
||||
func (sc *snellCipher) Decrypter(salt []byte) (cipher.AEAD, error) {
|
||||
return sc.makeAEAD(argon2.IDKey(sc.psk, salt, 3, 8, 1, uint32(sc.KeySize())))
|
||||
}
|
91
component/snell/snell.go
Normal file
91
component/snell/snell.go
Normal file
@ -0,0 +1,91 @@
|
||||
package snell
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/Dreamacro/go-shadowsocks2/shadowaead"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
const (
|
||||
CommandPing byte = 0
|
||||
CommandConnect byte = 1
|
||||
|
||||
CommandTunnel byte = 0
|
||||
CommandError byte = 2
|
||||
|
||||
Version byte = 1
|
||||
)
|
||||
|
||||
var (
|
||||
bufferPool = sync.Pool{New: func() interface{} { return &bytes.Buffer{} }}
|
||||
)
|
||||
|
||||
type Snell struct {
|
||||
net.Conn
|
||||
buffer [1]byte
|
||||
reply bool
|
||||
}
|
||||
|
||||
func (s *Snell) Read(b []byte) (int, error) {
|
||||
if s.reply {
|
||||
return s.Conn.Read(b)
|
||||
}
|
||||
|
||||
s.reply = true
|
||||
if _, err := io.ReadFull(s.Conn, s.buffer[:]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if s.buffer[0] == CommandTunnel {
|
||||
return s.Conn.Read(b)
|
||||
} else if s.buffer[0] != CommandError {
|
||||
return 0, errors.New("Command not support")
|
||||
}
|
||||
|
||||
// CommandError
|
||||
if _, err := io.ReadFull(s.Conn, s.buffer[:]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
length := int(s.buffer[0])
|
||||
msg := make([]byte, length)
|
||||
|
||||
if _, err := io.ReadFull(s.Conn, msg); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return 0, errors.New(string(msg))
|
||||
}
|
||||
|
||||
func WriteHeader(conn net.Conn, host string, port uint) error {
|
||||
buf := bufferPool.Get().(*bytes.Buffer)
|
||||
buf.Reset()
|
||||
defer bufferPool.Put(buf)
|
||||
buf.WriteByte(Version)
|
||||
buf.WriteByte(CommandConnect)
|
||||
|
||||
// clientID length & id
|
||||
buf.WriteByte(0)
|
||||
|
||||
// host & port
|
||||
buf.WriteByte(uint8(len(host)))
|
||||
buf.WriteString(host)
|
||||
binary.Write(buf, binary.BigEndian, uint16(port))
|
||||
|
||||
if _, err := conn.Write(buf.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func StreamConn(conn net.Conn, psk []byte) net.Conn {
|
||||
cipher := &snellCipher{psk, chacha20poly1305.New}
|
||||
return &Snell{Conn: shadowaead.NewConn(conn, cipher)}
|
||||
}
|
@ -41,7 +41,26 @@ const MaxAddrLen = 1 + 1 + 255 + 2
|
||||
const MaxAuthLen = 255
|
||||
|
||||
// Addr represents a SOCKS address as defined in RFC 1928 section 5.
|
||||
type Addr = []byte
|
||||
type Addr []byte
|
||||
|
||||
func (a Addr) String() string {
|
||||
var host, port string
|
||||
|
||||
switch a[0] {
|
||||
case AtypDomainName:
|
||||
hostLen := uint16(a[1])
|
||||
host = string(a[2 : 2+hostLen])
|
||||
port = strconv.Itoa((int(a[2+hostLen]) << 8) | int(a[2+hostLen+1]))
|
||||
case AtypIPv4:
|
||||
host = net.IP(a[1 : 1+net.IPv4len]).String()
|
||||
port = strconv.Itoa((int(a[1+net.IPv4len]) << 8) | int(a[1+net.IPv4len+1]))
|
||||
case AtypIPv6:
|
||||
host = net.IP(a[1 : 1+net.IPv6len]).String()
|
||||
port = strconv.Itoa((int(a[1+net.IPv6len]) << 8) | int(a[1+net.IPv6len+1]))
|
||||
}
|
||||
|
||||
return net.JoinHostPort(host, port)
|
||||
}
|
||||
|
||||
// SOCKS errors as defined in RFC 1928 section 6.
|
||||
const (
|
||||
@ -138,23 +157,33 @@ func ServerHandshake(rw net.Conn, authenticator auth.Authenticator) (addr Addr,
|
||||
return
|
||||
}
|
||||
|
||||
if buf[1] != CmdConnect && buf[1] != CmdUDPAssociate {
|
||||
err = ErrCommandNotSupported
|
||||
return
|
||||
}
|
||||
|
||||
command = buf[1]
|
||||
addr, err = readAddr(rw, buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// write VER REP RSV ATYP BND.ADDR BND.PORT
|
||||
_, err = rw.Write([]byte{5, 0, 0, 1, 0, 0, 0, 0, 0, 0})
|
||||
|
||||
switch command {
|
||||
case CmdConnect, CmdUDPAssociate:
|
||||
// Acquire server listened address info
|
||||
localAddr := ParseAddr(rw.LocalAddr().String())
|
||||
if localAddr == nil {
|
||||
err = ErrAddressNotSupported
|
||||
} else {
|
||||
// write VER REP RSV ATYP BND.ADDR BND.PORT
|
||||
_, err = rw.Write(bytes.Join([][]byte{{5, 0, 0}, localAddr}, []byte{}))
|
||||
}
|
||||
case CmdBind:
|
||||
fallthrough
|
||||
default:
|
||||
err = ErrCommandNotSupported
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ClientHandshake fast-tracks SOCKS initialization to get target address to connect on client side.
|
||||
func ClientHandshake(rw io.ReadWriter, addr Addr, cammand Command, user *User) error {
|
||||
func ClientHandshake(rw io.ReadWriter, addr Addr, command Command, user *User) (Addr, error) {
|
||||
buf := make([]byte, MaxAddrLen)
|
||||
var err error
|
||||
|
||||
@ -165,16 +194,16 @@ func ClientHandshake(rw io.ReadWriter, addr Addr, cammand Command, user *User) e
|
||||
_, err = rw.Write([]byte{5, 1, 0})
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// VER, METHOD
|
||||
if _, err := io.ReadFull(rw, buf[:2]); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if buf[0] != 5 {
|
||||
return errors.New("SOCKS version error")
|
||||
return nil, errors.New("SOCKS version error")
|
||||
}
|
||||
|
||||
if buf[1] == 2 {
|
||||
@ -187,30 +216,31 @@ func ClientHandshake(rw io.ReadWriter, addr Addr, cammand Command, user *User) e
|
||||
authMsg.WriteString(user.Password)
|
||||
|
||||
if _, err := rw.Write(authMsg.Bytes()); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(rw, buf[:2]); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if buf[1] != 0 {
|
||||
return errors.New("rejected username/password")
|
||||
return nil, errors.New("rejected username/password")
|
||||
}
|
||||
} else if buf[1] != 0 {
|
||||
return errors.New("SOCKS need auth")
|
||||
return nil, errors.New("SOCKS need auth")
|
||||
}
|
||||
|
||||
// VER, CMD, RSV, ADDR
|
||||
if _, err := rw.Write(bytes.Join([][]byte{{5, cammand, 0}, addr}, []byte(""))); err != nil {
|
||||
return err
|
||||
if _, err := rw.Write(bytes.Join([][]byte{{5, command, 0}, addr}, []byte{})); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(rw, buf[:10]); err != nil {
|
||||
return err
|
||||
// VER, REP, RSV
|
||||
if _, err := io.ReadFull(rw, buf[:3]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil
|
||||
return readAddr(rw, buf)
|
||||
}
|
||||
|
||||
func readAddr(r io.Reader, b []byte) (Addr, error) {
|
||||
@ -307,3 +337,39 @@ func ParseAddr(s string) Addr {
|
||||
|
||||
return addr
|
||||
}
|
||||
|
||||
// DecodeUDPPacket split `packet` to addr payload, and this function is mutable with `packet`
|
||||
func DecodeUDPPacket(packet []byte) (addr Addr, payload []byte, err error) {
|
||||
if len(packet) < 5 {
|
||||
err = errors.New("insufficient length of packet")
|
||||
return
|
||||
}
|
||||
|
||||
// packet[0] and packet[1] are reserved
|
||||
if !bytes.Equal(packet[:2], []byte{0, 0}) {
|
||||
err = errors.New("reserved fields should be zero")
|
||||
return
|
||||
}
|
||||
|
||||
if packet[2] != 0 /* fragments */ {
|
||||
err = errors.New("discarding fragmented payload")
|
||||
return
|
||||
}
|
||||
|
||||
addr = SplitAddr(packet[3:])
|
||||
if addr == nil {
|
||||
err = errors.New("failed to read UDP header")
|
||||
}
|
||||
|
||||
payload = packet[3+len(addr):]
|
||||
return
|
||||
}
|
||||
|
||||
func EncodeUDPPacket(addr Addr, payload []byte) (packet []byte, err error) {
|
||||
if addr == nil {
|
||||
err = errors.New("address is invalid")
|
||||
return
|
||||
}
|
||||
packet = bytes.Join([][]byte{{0, 0, 0}, addr, payload}, []byte{})
|
||||
return
|
||||
}
|
||||
|
@ -3,25 +3,32 @@ package obfs
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/Dreamacro/clash/component/vmess"
|
||||
)
|
||||
|
||||
// WebsocketOption is options of websocket obfs
|
||||
type WebsocketOption struct {
|
||||
// Option is options of websocket obfs
|
||||
type Option struct {
|
||||
Host string
|
||||
Path string
|
||||
Headers map[string]string
|
||||
TLSConfig *tls.Config
|
||||
Mux bool
|
||||
}
|
||||
|
||||
// NewWebsocketObfs return a HTTPObfs
|
||||
func NewWebsocketObfs(conn net.Conn, option *WebsocketOption) (net.Conn, error) {
|
||||
// NewV2rayObfs return a HTTPObfs
|
||||
func NewV2rayObfs(conn net.Conn, option *Option) (net.Conn, error) {
|
||||
header := http.Header{}
|
||||
for k, v := range option.Headers {
|
||||
header.Add(k, v)
|
||||
}
|
||||
|
||||
config := &vmess.WebsocketConfig{
|
||||
Host: option.Host,
|
||||
Path: option.Path,
|
||||
TLS: option.TLSConfig != nil,
|
||||
Headers: option.Headers,
|
||||
Headers: header,
|
||||
TLSConfig: option.TLSConfig,
|
||||
}
|
||||
|
||||
@ -30,10 +37,13 @@ func NewWebsocketObfs(conn net.Conn, option *WebsocketOption) (net.Conn, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn = NewMux(conn, MuxOption{
|
||||
ID: [2]byte{0, 0},
|
||||
Host: "127.0.0.1",
|
||||
Port: 0,
|
||||
})
|
||||
|
||||
if option.Mux {
|
||||
conn = NewMux(conn, MuxOption{
|
||||
ID: [2]byte{0, 0},
|
||||
Host: "127.0.0.1",
|
||||
Port: 0,
|
||||
})
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
@ -86,7 +87,7 @@ type Config struct {
|
||||
WebSocketPath string
|
||||
WebSocketHeaders map[string]string
|
||||
SkipCertVerify bool
|
||||
SessionCacahe tls.ClientSessionCache
|
||||
SessionCache tls.ClientSessionCache
|
||||
}
|
||||
|
||||
// New return a Conn with net.Conn and DstAddr
|
||||
@ -132,6 +133,11 @@ func NewClient(config Config) (*Client, error) {
|
||||
return nil, fmt.Errorf("Unknown network type: %s", config.NetWork)
|
||||
}
|
||||
|
||||
header := http.Header{}
|
||||
for k, v := range config.WebSocketHeaders {
|
||||
header.Add(k, v)
|
||||
}
|
||||
|
||||
host := net.JoinHostPort(config.HostName, config.Port)
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
@ -139,11 +145,14 @@ func NewClient(config Config) (*Client, error) {
|
||||
tlsConfig = &tls.Config{
|
||||
ServerName: config.HostName,
|
||||
InsecureSkipVerify: config.SkipCertVerify,
|
||||
ClientSessionCache: config.SessionCacahe,
|
||||
ClientSessionCache: config.SessionCache,
|
||||
}
|
||||
if tlsConfig.ClientSessionCache == nil {
|
||||
tlsConfig.ClientSessionCache = getClientSessionCache()
|
||||
}
|
||||
if host := header.Get("Host"); host != "" {
|
||||
tlsConfig.ServerName = host
|
||||
}
|
||||
}
|
||||
|
||||
var wsConfig *WebsocketConfig
|
||||
@ -151,7 +160,7 @@ func NewClient(config Config) (*Client, error) {
|
||||
wsConfig = &WebsocketConfig{
|
||||
Host: host,
|
||||
Path: config.WebSocketPath,
|
||||
Headers: config.WebSocketHeaders,
|
||||
Headers: header,
|
||||
TLS: config.TLS,
|
||||
TLSConfig: tlsConfig,
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ type websocketConn struct {
|
||||
type WebsocketConfig struct {
|
||||
Host string
|
||||
Path string
|
||||
Headers map[string]string
|
||||
Headers http.Header
|
||||
TLS bool
|
||||
TLSConfig *tls.Config
|
||||
}
|
||||
@ -131,14 +131,14 @@ func NewWebsocketConn(conn net.Conn, c *WebsocketConfig) (net.Conn, error) {
|
||||
|
||||
headers := http.Header{}
|
||||
if c.Headers != nil {
|
||||
for k, v := range c.Headers {
|
||||
headers.Set(k, v)
|
||||
for k := range c.Headers {
|
||||
headers.Add(k, c.Headers.Get(k))
|
||||
}
|
||||
}
|
||||
|
||||
wsConn, resp, err := dialer.Dial(uri.String(), headers)
|
||||
if err != nil {
|
||||
var reason string
|
||||
reason := err.Error()
|
||||
if resp != nil {
|
||||
reason = resp.Status
|
||||
}
|
||||
|
135
config/config.go
135
config/config.go
@ -12,6 +12,7 @@ import (
|
||||
adapters "github.com/Dreamacro/clash/adapters/outbound"
|
||||
"github.com/Dreamacro/clash/common/structure"
|
||||
"github.com/Dreamacro/clash/component/auth"
|
||||
trie "github.com/Dreamacro/clash/component/domain-trie"
|
||||
"github.com/Dreamacro/clash/component/fakeip"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/dns"
|
||||
@ -29,6 +30,7 @@ type General struct {
|
||||
RedirPort int `json:"redir-port"`
|
||||
Authentication []string `json:"authentication"`
|
||||
AllowLan bool `json:"allow-lan"`
|
||||
BindAddress string `json:"bind-address"`
|
||||
Mode T.Mode `json:"mode"`
|
||||
LogLevel log.LogLevel `json:"log-level"`
|
||||
ExternalController string `json:"-"`
|
||||
@ -38,13 +40,20 @@ type General struct {
|
||||
|
||||
// DNS config
|
||||
type DNS struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
IPv6 bool `yaml:"ipv6"`
|
||||
NameServer []dns.NameServer `yaml:"nameserver"`
|
||||
Fallback []dns.NameServer `yaml:"fallback"`
|
||||
Listen string `yaml:"listen"`
|
||||
EnhancedMode dns.EnhancedMode `yaml:"enhanced-mode"`
|
||||
FakeIPRange *fakeip.Pool
|
||||
Enable bool `yaml:"enable"`
|
||||
IPv6 bool `yaml:"ipv6"`
|
||||
NameServer []dns.NameServer `yaml:"nameserver"`
|
||||
Fallback []dns.NameServer `yaml:"fallback"`
|
||||
FallbackFilter FallbackFilter `yaml:"fallback-filter"`
|
||||
Listen string `yaml:"listen"`
|
||||
EnhancedMode dns.EnhancedMode `yaml:"enhanced-mode"`
|
||||
FakeIPRange *fakeip.Pool
|
||||
}
|
||||
|
||||
// FallbackFilter config
|
||||
type FallbackFilter struct {
|
||||
GeoIP bool `yaml:"geoip"`
|
||||
IPCIDR []*net.IPNet `yaml:"ipcidr"`
|
||||
}
|
||||
|
||||
// Experimental config
|
||||
@ -57,19 +66,26 @@ type Config struct {
|
||||
General *General
|
||||
DNS *DNS
|
||||
Experimental *Experimental
|
||||
Hosts *trie.Trie
|
||||
Rules []C.Rule
|
||||
Users []auth.AuthUser
|
||||
Proxies map[string]C.Proxy
|
||||
}
|
||||
|
||||
type rawDNS struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
IPv6 bool `yaml:"ipv6"`
|
||||
NameServer []string `yaml:"nameserver"`
|
||||
Fallback []string `yaml:"fallback"`
|
||||
Listen string `yaml:"listen"`
|
||||
EnhancedMode dns.EnhancedMode `yaml:"enhanced-mode"`
|
||||
FakeIPRange string `yaml:"fake-ip-range"`
|
||||
Enable bool `yaml:"enable"`
|
||||
IPv6 bool `yaml:"ipv6"`
|
||||
NameServer []string `yaml:"nameserver"`
|
||||
Fallback []string `yaml:"fallback"`
|
||||
FallbackFilter rawFallbackFilter `yaml:"fallback-filter"`
|
||||
Listen string `yaml:"listen"`
|
||||
EnhancedMode dns.EnhancedMode `yaml:"enhanced-mode"`
|
||||
FakeIPRange string `yaml:"fake-ip-range"`
|
||||
}
|
||||
|
||||
type rawFallbackFilter struct {
|
||||
GeoIP bool `yaml:"geoip"`
|
||||
IPCIDR []string `yaml:"ipcidr"`
|
||||
}
|
||||
|
||||
type rawConfig struct {
|
||||
@ -78,12 +94,14 @@ type rawConfig struct {
|
||||
RedirPort int `yaml:"redir-port"`
|
||||
Authentication []string `yaml:"authentication"`
|
||||
AllowLan bool `yaml:"allow-lan"`
|
||||
BindAddress string `yaml:"bind-address"`
|
||||
Mode T.Mode `yaml:"mode"`
|
||||
LogLevel log.LogLevel `yaml:"log-level"`
|
||||
ExternalController string `yaml:"external-controller"`
|
||||
ExternalUI string `yaml:"external-ui"`
|
||||
Secret string `yaml:"secret"`
|
||||
|
||||
Hosts map[string]string `yaml:"hosts"`
|
||||
DNS rawDNS `yaml:"dns"`
|
||||
Experimental Experimental `yaml:"experimental"`
|
||||
Proxy []map[string]interface{} `yaml:"Proxy"`
|
||||
@ -103,7 +121,11 @@ func readRawConfig(path string) ([]byte, error) {
|
||||
}
|
||||
|
||||
path = path[:len(path)-5] + ".yml"
|
||||
return ioutil.ReadFile(path)
|
||||
if _, err = os.Stat(path); err == nil {
|
||||
return ioutil.ReadFile(path)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func readConfig(path string) (*rawConfig, error) {
|
||||
@ -122,9 +144,11 @@ func readConfig(path string) (*rawConfig, error) {
|
||||
// config with some default value
|
||||
rawConfig := &rawConfig{
|
||||
AllowLan: false,
|
||||
BindAddress: "*",
|
||||
Mode: T.Rule,
|
||||
Authentication: []string{},
|
||||
LogLevel: log.INFO,
|
||||
Hosts: map[string]string{},
|
||||
Rule: []string{},
|
||||
Proxy: []map[string]interface{}{},
|
||||
ProxyGroup: []map[string]interface{}{},
|
||||
@ -134,6 +158,10 @@ func readConfig(path string) (*rawConfig, error) {
|
||||
DNS: rawDNS{
|
||||
Enable: false,
|
||||
FakeIPRange: "198.18.0.1/16",
|
||||
FallbackFilter: rawFallbackFilter{
|
||||
GeoIP: true,
|
||||
IPCIDR: []string{},
|
||||
},
|
||||
},
|
||||
}
|
||||
err = yaml.Unmarshal([]byte(data), &rawConfig)
|
||||
@ -174,6 +202,12 @@ func Parse(path string) (*Config, error) {
|
||||
}
|
||||
config.DNS = dnsCfg
|
||||
|
||||
hosts, err := parseHosts(rawCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Hosts = hosts
|
||||
|
||||
config.Users = parseAuthentication(rawCfg.Authentication)
|
||||
|
||||
return config, nil
|
||||
@ -184,6 +218,7 @@ func parseGeneral(cfg *rawConfig) (*General, error) {
|
||||
socksPort := cfg.SocksPort
|
||||
redirPort := cfg.RedirPort
|
||||
allowLan := cfg.AllowLan
|
||||
bindAddress := cfg.BindAddress
|
||||
externalController := cfg.ExternalController
|
||||
externalUI := cfg.ExternalUI
|
||||
secret := cfg.Secret
|
||||
@ -205,6 +240,7 @@ func parseGeneral(cfg *rawConfig) (*General, error) {
|
||||
SocksPort: socksPort,
|
||||
RedirPort: redirPort,
|
||||
AllowLan: allowLan,
|
||||
BindAddress: bindAddress,
|
||||
Mode: mode,
|
||||
LogLevel: logLevel,
|
||||
ExternalController: externalController,
|
||||
@ -264,6 +300,13 @@ func parseProxies(cfg *rawConfig) (map[string]C.Proxy, error) {
|
||||
break
|
||||
}
|
||||
proxy, err = adapters.NewVmess(*vmessOption)
|
||||
case "snell":
|
||||
snellOption := &adapters.SnellOption{}
|
||||
err = decoder.Decode(mapping, snellOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy, err = adapters.NewSnell(*snellOption)
|
||||
default:
|
||||
return nil, fmt.Errorf("Unsupport proxy type: %s", proxyType)
|
||||
}
|
||||
@ -279,12 +322,26 @@ func parseProxies(cfg *rawConfig) (map[string]C.Proxy, error) {
|
||||
proxyList = append(proxyList, proxy.Name())
|
||||
}
|
||||
|
||||
// parse proxy group
|
||||
// keep the origional order of ProxyGroups in config file
|
||||
for idx, mapping := range groupsConfig {
|
||||
groupType, existType := mapping["type"].(string)
|
||||
groupName, existName := mapping["name"].(string)
|
||||
if !existType && existName {
|
||||
return nil, fmt.Errorf("ProxyGroup %d: missing type or name", idx)
|
||||
if !existName {
|
||||
return nil, fmt.Errorf("ProxyGroup %d: missing name", idx)
|
||||
}
|
||||
proxyList = append(proxyList, groupName)
|
||||
}
|
||||
|
||||
// check if any loop exists and sort the ProxyGroups
|
||||
if err := proxyGroupsDagSort(groupsConfig, decoder); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// parse proxy group
|
||||
for _, mapping := range groupsConfig {
|
||||
groupType, existType := mapping["type"].(string)
|
||||
groupName, _ := mapping["name"].(string)
|
||||
if !existType {
|
||||
return nil, fmt.Errorf("ProxyGroup %s: missing type", groupName)
|
||||
}
|
||||
|
||||
if _, exist := proxies[groupName]; exist {
|
||||
@ -348,7 +405,6 @@ func parseProxies(cfg *rawConfig) (map[string]C.Proxy, error) {
|
||||
return nil, fmt.Errorf("Proxy %s: %s", groupName, err.Error())
|
||||
}
|
||||
proxies[groupName] = adapters.NewProxy(group)
|
||||
proxyList = append(proxyList, groupName)
|
||||
}
|
||||
|
||||
ps := []C.Proxy{}
|
||||
@ -434,6 +490,21 @@ func parseRules(cfg *rawConfig, proxies map[string]C.Proxy) ([]C.Rule, error) {
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func parseHosts(cfg *rawConfig) (*trie.Trie, error) {
|
||||
tree := trie.New()
|
||||
if len(cfg.Hosts) != 0 {
|
||||
for domain, ipStr := range cfg.Hosts {
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("%s is not a valid IP", ipStr)
|
||||
}
|
||||
tree.Insert(domain, ip)
|
||||
}
|
||||
}
|
||||
|
||||
return tree, nil
|
||||
}
|
||||
|
||||
func hostWithDefaultPort(host string, defPort string) (string, error) {
|
||||
if !strings.Contains(host, ":") {
|
||||
host += ":"
|
||||
@ -498,6 +569,20 @@ func parseNameServer(servers []string) ([]dns.NameServer, error) {
|
||||
return nameservers, nil
|
||||
}
|
||||
|
||||
func parseFallbackIPCIDR(ips []string) ([]*net.IPNet, error) {
|
||||
ipNets := []*net.IPNet{}
|
||||
|
||||
for idx, ip := range ips {
|
||||
_, ipnet, err := net.ParseCIDR(ip)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("DNS FallbackIP[%d] format error: %s", idx, err.Error())
|
||||
}
|
||||
ipNets = append(ipNets, ipnet)
|
||||
}
|
||||
|
||||
return ipNets, nil
|
||||
}
|
||||
|
||||
func parseDNS(cfg rawDNS) (*DNS, error) {
|
||||
if cfg.Enable && len(cfg.NameServer) == 0 {
|
||||
return nil, fmt.Errorf("If DNS configuration is turned on, NameServer cannot be empty")
|
||||
@ -508,6 +593,9 @@ func parseDNS(cfg rawDNS) (*DNS, error) {
|
||||
Listen: cfg.Listen,
|
||||
IPv6: cfg.IPv6,
|
||||
EnhancedMode: cfg.EnhancedMode,
|
||||
FallbackFilter: FallbackFilter{
|
||||
IPCIDR: []*net.IPNet{},
|
||||
},
|
||||
}
|
||||
var err error
|
||||
if dnsCfg.NameServer, err = parseNameServer(cfg.NameServer); err != nil {
|
||||
@ -523,7 +611,7 @@ func parseDNS(cfg rawDNS) (*DNS, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pool, err := fakeip.New(ipnet)
|
||||
pool, err := fakeip.New(ipnet, 1000)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -531,6 +619,11 @@ func parseDNS(cfg rawDNS) (*DNS, error) {
|
||||
dnsCfg.FakeIPRange = pool
|
||||
}
|
||||
|
||||
dnsCfg.FallbackFilter.GeoIP = cfg.FallbackFilter.GeoIP
|
||||
if fallbackip, err := parseFallbackIPCIDR(cfg.FallbackFilter.IPCIDR); err == nil {
|
||||
dnsCfg.FallbackFilter.IPCIDR = fallbackip
|
||||
}
|
||||
|
||||
return dnsCfg, nil
|
||||
}
|
||||
|
||||
|
123
config/utils.go
123
config/utils.go
@ -4,6 +4,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
adapters "github.com/Dreamacro/clash/adapters/outbound"
|
||||
"github.com/Dreamacro/clash/common/structure"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
@ -34,3 +36,124 @@ func or(pointers ...*int) *int {
|
||||
}
|
||||
return pointers[len(pointers)-1]
|
||||
}
|
||||
|
||||
// Check if ProxyGroups form DAG(Directed Acyclic Graph), and sort all ProxyGroups by dependency order.
|
||||
// Meanwhile, record the original index in the config file.
|
||||
// If loop is detected, return an error with location of loop.
|
||||
func proxyGroupsDagSort(groupsConfig []map[string]interface{}, decoder *structure.Decoder) error {
|
||||
|
||||
type graphNode struct {
|
||||
indegree int
|
||||
// topological order
|
||||
topo int
|
||||
// the origional data in `groupsConfig`
|
||||
data map[string]interface{}
|
||||
// `outdegree` and `from` are used in loop locating
|
||||
outdegree int
|
||||
from []string
|
||||
}
|
||||
|
||||
graph := make(map[string]*graphNode)
|
||||
|
||||
// Step 1.1 build dependency graph
|
||||
for _, mapping := range groupsConfig {
|
||||
option := &adapters.ProxyGroupOption{}
|
||||
err := decoder.Decode(mapping, option)
|
||||
groupName := option.Name
|
||||
if err != nil {
|
||||
return fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error())
|
||||
}
|
||||
|
||||
if node, ok := graph[groupName]; ok {
|
||||
if node.data != nil {
|
||||
return fmt.Errorf("ProxyGroup %s: duplicate group name", groupName)
|
||||
}
|
||||
node.data = mapping
|
||||
} else {
|
||||
graph[groupName] = &graphNode{0, -1, mapping, 0, nil}
|
||||
}
|
||||
|
||||
for _, proxy := range option.Proxies {
|
||||
if node, ex := graph[proxy]; ex {
|
||||
node.indegree++
|
||||
} else {
|
||||
graph[proxy] = &graphNode{1, -1, nil, 0, nil}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Step 1.2 Topological Sort
|
||||
// topological index of **ProxyGroup**
|
||||
index := 0
|
||||
queue := make([]string, 0)
|
||||
for name, node := range graph {
|
||||
// in the begning, put nodes that have `node.indegree == 0` into queue.
|
||||
if node.indegree == 0 {
|
||||
queue = append(queue, name)
|
||||
}
|
||||
}
|
||||
// every element in queue have indegree == 0
|
||||
for ; len(queue) > 0; queue = queue[1:] {
|
||||
name := queue[0]
|
||||
node := graph[name]
|
||||
if node.data != nil {
|
||||
index++
|
||||
groupsConfig[len(groupsConfig)-index] = node.data
|
||||
for _, proxy := range node.data["proxies"].([]interface{}) {
|
||||
child := graph[proxy.(string)]
|
||||
child.indegree--
|
||||
if child.indegree == 0 {
|
||||
queue = append(queue, proxy.(string))
|
||||
}
|
||||
}
|
||||
}
|
||||
delete(graph, name)
|
||||
}
|
||||
|
||||
// no loop is detected, return sorted ProxyGroup
|
||||
if len(graph) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// if loop is detected, locate the loop and throw an error
|
||||
// Step 2.1 rebuild the graph, fill `outdegree` and `from` filed
|
||||
for name, node := range graph {
|
||||
if node.data == nil {
|
||||
continue
|
||||
}
|
||||
for _, proxy := range node.data["proxies"].([]interface{}) {
|
||||
node.outdegree++
|
||||
child := graph[proxy.(string)]
|
||||
if child.from == nil {
|
||||
child.from = make([]string, 0, child.indegree)
|
||||
}
|
||||
child.from = append(child.from, name)
|
||||
}
|
||||
}
|
||||
// Step 2.2 remove nodes outside the loop. so that we have only the loops remain in `graph`
|
||||
queue = make([]string, 0)
|
||||
// initialize queue with node have outdegree == 0
|
||||
for name, node := range graph {
|
||||
if node.outdegree == 0 {
|
||||
queue = append(queue, name)
|
||||
}
|
||||
}
|
||||
// every element in queue have outdegree == 0
|
||||
for ; len(queue) > 0; queue = queue[1:] {
|
||||
name := queue[0]
|
||||
node := graph[name]
|
||||
for _, f := range node.from {
|
||||
graph[f].outdegree--
|
||||
if graph[f].outdegree == 0 {
|
||||
queue = append(queue, f)
|
||||
}
|
||||
}
|
||||
delete(graph, name)
|
||||
}
|
||||
// Step 2.3 report the elements in loop
|
||||
loopElements := make([]string, 0, len(graph))
|
||||
for name := range graph {
|
||||
loopElements = append(loopElements, name)
|
||||
delete(graph, name)
|
||||
}
|
||||
return fmt.Errorf("Loop is detected in ProxyGroup, please check following ProxyGroups: %v", loopElements)
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package constant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
@ -12,6 +14,7 @@ const (
|
||||
Reject
|
||||
Selector
|
||||
Shadowsocks
|
||||
Snell
|
||||
Socks5
|
||||
Http
|
||||
URLTest
|
||||
@ -24,11 +27,39 @@ type ServerAdapter interface {
|
||||
Metadata() *Metadata
|
||||
}
|
||||
|
||||
type Connection interface {
|
||||
Chains() Chain
|
||||
AppendToChains(adapter ProxyAdapter)
|
||||
}
|
||||
|
||||
type Chain []string
|
||||
|
||||
func (c Chain) String() string {
|
||||
switch len(c) {
|
||||
case 0:
|
||||
return ""
|
||||
case 1:
|
||||
return c[0]
|
||||
default:
|
||||
return fmt.Sprintf("%s[%s]", c[len(c)-1], c[0])
|
||||
}
|
||||
}
|
||||
|
||||
type Conn interface {
|
||||
net.Conn
|
||||
Connection
|
||||
}
|
||||
|
||||
type PacketConn interface {
|
||||
net.PacketConn
|
||||
Connection
|
||||
}
|
||||
|
||||
type ProxyAdapter interface {
|
||||
Name() string
|
||||
Type() AdapterType
|
||||
Dial(metadata *Metadata) (net.Conn, error)
|
||||
DialUDP(metadata *Metadata) (net.PacketConn, net.Addr, error)
|
||||
DialContext(ctx context.Context, metadata *Metadata) (Conn, error)
|
||||
DialUDP(metadata *Metadata) (PacketConn, net.Addr, error)
|
||||
SupportUDP() bool
|
||||
Destroy()
|
||||
MarshalJSON() ([]byte, error)
|
||||
@ -43,8 +74,9 @@ type Proxy interface {
|
||||
ProxyAdapter
|
||||
Alive() bool
|
||||
DelayHistory() []DelayHistory
|
||||
Dial(metadata *Metadata) (Conn, error)
|
||||
LastDelay() uint16
|
||||
URLTest(url string) (uint16, error)
|
||||
URLTest(ctx context.Context, url string) (uint16, error)
|
||||
}
|
||||
|
||||
// AdapterType is enum of adapter type
|
||||
@ -62,6 +94,8 @@ func (at AdapterType) String() string {
|
||||
return "Selector"
|
||||
case Shadowsocks:
|
||||
return "Shadowsocks"
|
||||
case Snell:
|
||||
return "Snell"
|
||||
case Socks5:
|
||||
return "Socks5"
|
||||
case Http:
|
||||
@ -73,6 +107,6 @@ func (at AdapterType) String() string {
|
||||
case LoadBalance:
|
||||
return "LoadBalance"
|
||||
default:
|
||||
return "Unknow"
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
@ -41,11 +41,18 @@ type Metadata struct {
|
||||
Host string
|
||||
}
|
||||
|
||||
func (m *Metadata) RemoteAddress() string {
|
||||
return net.JoinHostPort(m.String(), m.DstPort)
|
||||
}
|
||||
|
||||
func (m *Metadata) String() string {
|
||||
if m.Host == "" {
|
||||
if m.Host != "" {
|
||||
return m.Host
|
||||
} else if m.DstIP != nil {
|
||||
return m.DstIP.String()
|
||||
} else {
|
||||
return "<nil>"
|
||||
}
|
||||
return m.Host
|
||||
}
|
||||
|
||||
func (m *Metadata) Valid() bool {
|
||||
|
@ -2,7 +2,6 @@ package constant
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
P "path"
|
||||
)
|
||||
|
||||
@ -16,16 +15,9 @@ type path struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
currentUser, err := user.Current()
|
||||
var homedir string
|
||||
homedir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
dir := os.Getenv("HOME")
|
||||
if dir == "" {
|
||||
dir, _ = os.Getwd()
|
||||
}
|
||||
homedir = dir
|
||||
} else {
|
||||
homedir = currentUser.HomeDir
|
||||
homedir, _ = os.Getwd()
|
||||
}
|
||||
|
||||
homedir = P.Join(homedir, ".config", Name)
|
||||
|
@ -36,7 +36,7 @@ func (rt RuleType) String() string {
|
||||
case MATCH:
|
||||
return "MATCH"
|
||||
default:
|
||||
return "Unknow"
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
|
26
dns/filters.go
Normal file
26
dns/filters.go
Normal file
@ -0,0 +1,26 @@
|
||||
package dns
|
||||
|
||||
import "net"
|
||||
|
||||
type fallbackFilter interface {
|
||||
Match(net.IP) bool
|
||||
}
|
||||
|
||||
type geoipFilter struct{}
|
||||
|
||||
func (gf *geoipFilter) Match(ip net.IP) bool {
|
||||
if mmdb == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
record, _ := mmdb.Country(ip)
|
||||
return record.Country.IsoCode == "CN" || record.Country.IsoCode == ""
|
||||
}
|
||||
|
||||
type ipnetFilter struct {
|
||||
ipnet *net.IPNet
|
||||
}
|
||||
|
||||
func (inf *ipnetFilter) Match(ip net.IP) bool {
|
||||
return inf.ipnet.Contains(ip)
|
||||
}
|
@ -3,14 +3,88 @@ package dns
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
errIPNotFound = errors.New("cannot found ip")
|
||||
errIPVersion = errors.New("ip version error")
|
||||
)
|
||||
|
||||
// ResolveIPv4 with a host, return ipv4
|
||||
func ResolveIPv4(host string) (net.IP, error) {
|
||||
if node := DefaultHosts.Search(host); node != nil {
|
||||
if ip := node.Data.(net.IP).To4(); ip != nil {
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
|
||||
ip := net.ParseIP(host)
|
||||
if ip != nil {
|
||||
if !strings.Contains(host, ":") {
|
||||
return ip, nil
|
||||
}
|
||||
return nil, errIPVersion
|
||||
}
|
||||
|
||||
if DefaultResolver != nil {
|
||||
return DefaultResolver.ResolveIPv4(host)
|
||||
}
|
||||
|
||||
ipAddrs, err := net.LookupIP(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, ip := range ipAddrs {
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
return ip4, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errIPNotFound
|
||||
}
|
||||
|
||||
// ResolveIPv6 with a host, return ipv6
|
||||
func ResolveIPv6(host string) (net.IP, error) {
|
||||
if node := DefaultHosts.Search(host); node != nil {
|
||||
if ip := node.Data.(net.IP).To16(); ip != nil {
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
|
||||
ip := net.ParseIP(host)
|
||||
if ip != nil {
|
||||
if strings.Contains(host, ":") {
|
||||
return ip, nil
|
||||
}
|
||||
return nil, errIPVersion
|
||||
}
|
||||
|
||||
if DefaultResolver != nil {
|
||||
return DefaultResolver.ResolveIPv6(host)
|
||||
}
|
||||
|
||||
ipAddrs, err := net.LookupIP(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, ip := range ipAddrs {
|
||||
if ip.To4() == nil {
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errIPNotFound
|
||||
}
|
||||
|
||||
// ResolveIP with a host, return ip
|
||||
func ResolveIP(host string) (net.IP, error) {
|
||||
if node := DefaultHosts.Search(host); node != nil {
|
||||
return node.Data.(net.IP), nil
|
||||
}
|
||||
|
||||
if DefaultResolver != nil {
|
||||
if DefaultResolver.ipv6 {
|
||||
return DefaultResolver.ResolveIP(host)
|
||||
|
79
dns/middleware.go
Normal file
79
dns/middleware.go
Normal file
@ -0,0 +1,79 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/Dreamacro/clash/component/fakeip"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
|
||||
D "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type handler func(w D.ResponseWriter, r *D.Msg)
|
||||
type middleware func(next handler) handler
|
||||
|
||||
func withFakeIP(fakePool *fakeip.Pool) middleware {
|
||||
return func(next handler) handler {
|
||||
return func(w D.ResponseWriter, r *D.Msg) {
|
||||
q := r.Question[0]
|
||||
|
||||
if q.Qtype == D.TypeAAAA {
|
||||
D.HandleFailed(w, r)
|
||||
return
|
||||
} else if q.Qtype != D.TypeA {
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
host := strings.TrimRight(q.Name, ".")
|
||||
|
||||
rr := &D.A{}
|
||||
rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeA, Class: D.ClassINET, Ttl: dnsDefaultTTL}
|
||||
ip := fakePool.Lookup(host)
|
||||
rr.A = ip
|
||||
msg := r.Copy()
|
||||
msg.Answer = []D.RR{rr}
|
||||
|
||||
setMsgTTL(msg, 1)
|
||||
msg.SetReply(r)
|
||||
w.WriteMsg(msg)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func withResolver(resolver *Resolver) handler {
|
||||
return func(w D.ResponseWriter, r *D.Msg) {
|
||||
msg, err := resolver.Exchange(r)
|
||||
if err != nil {
|
||||
q := r.Question[0]
|
||||
log.Debugln("[DNS Server] Exchange %s failed: %v", q.String(), err)
|
||||
D.HandleFailed(w, r)
|
||||
return
|
||||
}
|
||||
msg.SetReply(r)
|
||||
w.WriteMsg(msg)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func compose(middlewares []middleware, endpoint handler) handler {
|
||||
length := len(middlewares)
|
||||
h := endpoint
|
||||
for i := length - 1; i >= 0; i-- {
|
||||
middleware := middlewares[i]
|
||||
h = middleware(h)
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func newHandler(resolver *Resolver) handler {
|
||||
middlewares := []middleware{}
|
||||
|
||||
if resolver.IsFakeIP() {
|
||||
middlewares = append(middlewares, withFakeIP(resolver.pool))
|
||||
}
|
||||
|
||||
return compose(middlewares, withResolver(resolver))
|
||||
}
|
154
dns/resolver.go
154
dns/resolver.go
@ -11,16 +11,21 @@ import (
|
||||
|
||||
"github.com/Dreamacro/clash/common/cache"
|
||||
"github.com/Dreamacro/clash/common/picker"
|
||||
trie "github.com/Dreamacro/clash/component/domain-trie"
|
||||
"github.com/Dreamacro/clash/component/fakeip"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
D "github.com/miekg/dns"
|
||||
geoip2 "github.com/oschwald/geoip2-golang"
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultResolver aim to resolve ip with host
|
||||
// DefaultResolver aim to resolve ip
|
||||
DefaultResolver *Resolver
|
||||
|
||||
// DefaultHosts aim to resolve hosts
|
||||
DefaultHosts = trie.New()
|
||||
)
|
||||
|
||||
var (
|
||||
@ -41,22 +46,19 @@ type result struct {
|
||||
}
|
||||
|
||||
type Resolver struct {
|
||||
ipv6 bool
|
||||
mapping bool
|
||||
fakeip bool
|
||||
pool *fakeip.Pool
|
||||
fallback []resolver
|
||||
main []resolver
|
||||
cache *cache.Cache
|
||||
ipv6 bool
|
||||
mapping bool
|
||||
fakeip bool
|
||||
pool *fakeip.Pool
|
||||
main []resolver
|
||||
fallback []resolver
|
||||
fallbackFilters []fallbackFilter
|
||||
group singleflight.Group
|
||||
cache *cache.Cache
|
||||
}
|
||||
|
||||
// ResolveIP request with TypeA and TypeAAAA, priority return TypeAAAA
|
||||
func (r *Resolver) ResolveIP(host string) (ip net.IP, err error) {
|
||||
ip = net.ParseIP(host)
|
||||
if ip != nil {
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
ch := make(chan net.IP)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
@ -85,26 +87,21 @@ func (r *Resolver) ResolveIP(host string) (ip net.IP, err error) {
|
||||
|
||||
// ResolveIPv4 request with TypeA
|
||||
func (r *Resolver) ResolveIPv4(host string) (ip net.IP, err error) {
|
||||
ip = net.ParseIP(host)
|
||||
if ip != nil {
|
||||
return ip, nil
|
||||
return r.resolveIP(host, D.TypeA)
|
||||
}
|
||||
|
||||
// ResolveIPv6 request with TypeAAAA
|
||||
func (r *Resolver) ResolveIPv6(host string) (ip net.IP, err error) {
|
||||
return r.resolveIP(host, D.TypeAAAA)
|
||||
}
|
||||
|
||||
func (r *Resolver) shouldFallback(ip net.IP) bool {
|
||||
for _, filter := range r.fallbackFilters {
|
||||
if filter.Match(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
query := &D.Msg{}
|
||||
query.SetQuestion(D.Fqdn(host), D.TypeA)
|
||||
|
||||
msg, err := r.Exchange(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ips := r.msgToIP(msg)
|
||||
if len(ips) == 0 {
|
||||
return nil, errIPNotFound
|
||||
}
|
||||
|
||||
ip = ips[0]
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
// Exchange a batch of dns request, and it use cache
|
||||
@ -134,18 +131,29 @@ func (r *Resolver) Exchange(m *D.Msg) (msg *D.Msg, err error) {
|
||||
}
|
||||
}()
|
||||
|
||||
isIPReq := isIPRequest(q)
|
||||
if isIPReq {
|
||||
msg, err = r.fallbackExchange(m)
|
||||
return
|
||||
ret, err, _ := r.group.Do(q.String(), func() (interface{}, error) {
|
||||
isIPReq := isIPRequest(q)
|
||||
if isIPReq {
|
||||
msg, err := r.fallbackExchange(m)
|
||||
return msg, err
|
||||
}
|
||||
|
||||
return r.batchExchange(r.main, m)
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
msg = ret.(*D.Msg)
|
||||
}
|
||||
|
||||
msg, err = r.batchExchange(r.main, m)
|
||||
return
|
||||
}
|
||||
|
||||
// IPToHost return fake-ip or redir-host mapping host
|
||||
func (r *Resolver) IPToHost(ip net.IP) (string, bool) {
|
||||
if r.fakeip {
|
||||
return r.pool.LookBack(ip)
|
||||
}
|
||||
|
||||
cache := r.cache.Get(ip.String())
|
||||
if cache == nil {
|
||||
return "", false
|
||||
@ -163,32 +171,20 @@ func (r *Resolver) IsFakeIP() bool {
|
||||
}
|
||||
|
||||
func (r *Resolver) batchExchange(clients []resolver, m *D.Msg) (msg *D.Msg, err error) {
|
||||
in := make(chan interface{})
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
fast := picker.SelectFast(ctx, in)
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(clients))
|
||||
for _, r := range clients {
|
||||
go func(r resolver) {
|
||||
defer wg.Done()
|
||||
fast, ctx := picker.WithTimeout(context.Background(), time.Second)
|
||||
for _, client := range clients {
|
||||
r := client
|
||||
fast.Go(func() (interface{}, error) {
|
||||
msg, err := r.ExchangeContext(ctx, m)
|
||||
if err != nil || msg.Rcode != D.RcodeSuccess {
|
||||
return
|
||||
return nil, errors.New("resolve error")
|
||||
}
|
||||
in <- msg
|
||||
}(r)
|
||||
return msg, nil
|
||||
})
|
||||
}
|
||||
|
||||
// release in channel
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(in)
|
||||
}()
|
||||
|
||||
elm, exist := <-fast
|
||||
if !exist {
|
||||
elm := fast.Wait()
|
||||
if elm == nil {
|
||||
return nil, errors.New("All DNS requests failed")
|
||||
}
|
||||
|
||||
@ -206,13 +202,8 @@ func (r *Resolver) fallbackExchange(m *D.Msg) (msg *D.Msg, err error) {
|
||||
fallbackMsg := r.asyncExchange(r.fallback, m)
|
||||
res := <-msgCh
|
||||
if res.Error == nil {
|
||||
if mmdb == nil {
|
||||
return nil, errors.New("GeoIP cannot use")
|
||||
}
|
||||
|
||||
if ips := r.msgToIP(res.Msg); len(ips) != 0 {
|
||||
if record, _ := mmdb.Country(ips[0]); record.Country.IsoCode == "CN" || record.Country.IsoCode == "" {
|
||||
// release channel
|
||||
if r.shouldFallback(ips[0]) {
|
||||
go func() { <-fallbackMsg }()
|
||||
msg = res.Msg
|
||||
return msg, err
|
||||
@ -226,6 +217,16 @@ func (r *Resolver) fallbackExchange(m *D.Msg) (msg *D.Msg, err error) {
|
||||
}
|
||||
|
||||
func (r *Resolver) resolveIP(host string, dnsType uint16) (ip net.IP, err error) {
|
||||
ip = net.ParseIP(host)
|
||||
if ip != nil {
|
||||
isIPv4 := ip.To4() != nil
|
||||
if dnsType == D.TypeAAAA && !isIPv4 {
|
||||
return ip, nil
|
||||
} else if dnsType == D.TypeA && isIPv4 {
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
|
||||
query := &D.Msg{}
|
||||
query.SetQuestion(D.Fqdn(host), dnsType)
|
||||
|
||||
@ -272,18 +273,20 @@ type NameServer struct {
|
||||
Addr string
|
||||
}
|
||||
|
||||
type FallbackFilter struct {
|
||||
GeoIP bool
|
||||
IPCIDR []*net.IPNet
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Main, Fallback []NameServer
|
||||
IPv6 bool
|
||||
EnhancedMode EnhancedMode
|
||||
FallbackFilter FallbackFilter
|
||||
Pool *fakeip.Pool
|
||||
}
|
||||
|
||||
func New(config Config) *Resolver {
|
||||
once.Do(func() {
|
||||
mmdb, _ = geoip2.Open(C.Path.MMDB())
|
||||
})
|
||||
|
||||
r := &Resolver{
|
||||
ipv6: config.IPv6,
|
||||
main: transform(config.Main),
|
||||
@ -292,8 +295,23 @@ func New(config Config) *Resolver {
|
||||
fakeip: config.EnhancedMode == FAKEIP,
|
||||
pool: config.Pool,
|
||||
}
|
||||
|
||||
if len(config.Fallback) != 0 {
|
||||
r.fallback = transform(config.Fallback)
|
||||
}
|
||||
|
||||
fallbackFilters := []fallbackFilter{}
|
||||
if config.FallbackFilter.GeoIP {
|
||||
once.Do(func() {
|
||||
mmdb, _ = geoip2.Open(C.Path.MMDB())
|
||||
})
|
||||
|
||||
fallbackFilters = append(fallbackFilters, &geoipFilter{})
|
||||
}
|
||||
for _, ipnet := range config.FallbackFilter.IPCIDR {
|
||||
fallbackFilters = append(fallbackFilters, &ipnetFilter{ipnet: ipnet})
|
||||
}
|
||||
r.fallbackFilters = fallbackFilters
|
||||
|
||||
return r
|
||||
}
|
||||
|
@ -1,12 +1,8 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/Dreamacro/clash/log"
|
||||
"github.com/miekg/dns"
|
||||
D "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
@ -19,79 +15,26 @@ var (
|
||||
|
||||
type Server struct {
|
||||
*D.Server
|
||||
r *Resolver
|
||||
handler handler
|
||||
}
|
||||
|
||||
func (s *Server) ServeDNS(w D.ResponseWriter, r *D.Msg) {
|
||||
if s.r.IsFakeIP() {
|
||||
msg, err := s.handleFakeIP(r)
|
||||
if err != nil {
|
||||
D.HandleFailed(w, r)
|
||||
return
|
||||
}
|
||||
msg.SetReply(r)
|
||||
w.WriteMsg(msg)
|
||||
return
|
||||
}
|
||||
|
||||
msg, err := s.r.Exchange(r)
|
||||
|
||||
if err != nil {
|
||||
if len(r.Question) > 0 {
|
||||
q := r.Question[0]
|
||||
qString := fmt.Sprintf("%s %s %s", q.Name, D.Class(q.Qclass).String(), D.Type(q.Qtype).String())
|
||||
log.Debugln("[DNS Server] Exchange %s failed: %v", qString, err)
|
||||
}
|
||||
if len(r.Question) == 0 {
|
||||
D.HandleFailed(w, r)
|
||||
return
|
||||
}
|
||||
msg.SetReply(r)
|
||||
w.WriteMsg(msg)
|
||||
|
||||
s.handler(w, r)
|
||||
}
|
||||
|
||||
func (s *Server) handleFakeIP(r *D.Msg) (msg *D.Msg, err error) {
|
||||
if len(r.Question) == 0 {
|
||||
err = errors.New("should have one question at least")
|
||||
return
|
||||
}
|
||||
|
||||
q := r.Question[0]
|
||||
|
||||
cache := s.r.cache.Get("fakeip:" + q.String())
|
||||
if cache != nil {
|
||||
msg = cache.(*D.Msg).Copy()
|
||||
setMsgTTL(msg, 1)
|
||||
return
|
||||
}
|
||||
|
||||
var ip net.IP
|
||||
defer func() {
|
||||
if msg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
putMsgToCache(s.r.cache, "fakeip:"+q.String(), msg)
|
||||
putMsgToCache(s.r.cache, ip.String(), msg)
|
||||
|
||||
setMsgTTL(msg, 1)
|
||||
}()
|
||||
|
||||
rr := &D.A{}
|
||||
rr.Hdr = dns.RR_Header{Name: r.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: dnsDefaultTTL}
|
||||
ip = s.r.pool.Get()
|
||||
rr.A = ip
|
||||
msg = r.Copy()
|
||||
msg.Answer = []D.RR{rr}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Server) setReslover(r *Resolver) {
|
||||
s.r = r
|
||||
func (s *Server) setHandler(handler handler) {
|
||||
s.handler = handler
|
||||
}
|
||||
|
||||
func ReCreateServer(addr string, resolver *Resolver) error {
|
||||
if addr == address {
|
||||
server.setReslover(resolver)
|
||||
if addr == address && resolver != nil {
|
||||
handler := newHandler(resolver)
|
||||
server.setHandler(handler)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -116,7 +59,8 @@ func ReCreateServer(addr string, resolver *Resolver) error {
|
||||
}
|
||||
|
||||
address = addr
|
||||
server = &Server{r: resolver}
|
||||
handler := newHandler(resolver)
|
||||
server = &Server{handler: handler}
|
||||
server.Server = &D.Server{Addr: addr, PacketConn: p, Handler: server}
|
||||
|
||||
go func() {
|
||||
|
17
go.mod
17
go.mod
@ -1,20 +1,23 @@
|
||||
module github.com/Dreamacro/clash
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/Dreamacro/go-shadowsocks2 v0.1.3
|
||||
github.com/Dreamacro/go-shadowsocks2 v0.1.5-0.20191012162057-46254afc8b68
|
||||
github.com/eapache/queue v1.1.0 // indirect
|
||||
github.com/go-chi/chi v4.0.2+incompatible
|
||||
github.com/go-chi/cors v1.0.0
|
||||
github.com/go-chi/render v1.0.1
|
||||
github.com/gofrs/uuid v3.2.0+incompatible
|
||||
github.com/gorilla/websocket v1.4.0
|
||||
github.com/miekg/dns v1.1.14
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
github.com/miekg/dns v1.1.22
|
||||
github.com/oschwald/geoip2-golang v1.3.0
|
||||
github.com/oschwald/maxminddb-golang v1.3.1 // indirect
|
||||
github.com/oschwald/maxminddb-golang v1.5.0 // indirect
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect
|
||||
github.com/stretchr/testify v1.4.0
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
|
||||
golang.org/x/net v0.0.0-20191011234655-491137f69257
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
|
||||
gopkg.in/eapache/channels.v1 v1.1.0
|
||||
gopkg.in/yaml.v2 v2.2.2
|
||||
)
|
||||
|
41
go.sum
41
go.sum
@ -1,7 +1,8 @@
|
||||
github.com/Dreamacro/go-shadowsocks2 v0.1.3 h1:1ffY/q4e3o+MnztYgIq1iZiX1BWoWQ6D3AIO1kkb8bc=
|
||||
github.com/Dreamacro/go-shadowsocks2 v0.1.3/go.mod h1:0x17IhQ+mlY6q/ffKRpzaE7u4aHMxxnitTRSrV5G6TU=
|
||||
github.com/Dreamacro/go-shadowsocks2 v0.1.5-0.20191012162057-46254afc8b68 h1:UBDLpj1IGVkUcUBuZWE6DmZApPTZcnmcV6AfyDN/yhg=
|
||||
github.com/Dreamacro/go-shadowsocks2 v0.1.5-0.20191012162057-46254afc8b68/go.mod h1:Y8obOtHDOqxMGHjPglfCiXZBKExOA9VL6I6sJagOwYM=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
|
||||
@ -14,36 +15,56 @@ github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
|
||||
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/miekg/dns v1.1.14 h1:wkQWn9wIp4mZbwW8XV6Km6owkvRPbOiV004ZM2CkGvA=
|
||||
github.com/miekg/dns v1.1.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.22 h1:Jm64b3bO9kP43ddLjL2EY3Io6bmy1qGb9Xxz6TqS6rc=
|
||||
github.com/miekg/dns v1.1.22/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/oschwald/geoip2-golang v1.3.0 h1:D+Hsdos1NARPbzZ2aInUHZL+dApIzo8E0ErJVsWcku8=
|
||||
github.com/oschwald/geoip2-golang v1.3.0/go.mod h1:0LTTzix/Ao1uMvOhAV4iLU0Lz7eCrP94qZWBTDKf0iE=
|
||||
github.com/oschwald/maxminddb-golang v1.3.1 h1:kPc5+ieL5CC/Zn0IaXJPxDFlUxKTQEU8QBTtmfQDAIo=
|
||||
github.com/oschwald/maxminddb-golang v1.3.1/go.mod h1:3jhIUymTJ5VREKyIhWm66LJiQt04F0UCDdodShpjWsY=
|
||||
github.com/oschwald/maxminddb-golang v1.5.0 h1:rmyoIV6z2/s9TCJedUuDiKht2RN12LWJ1L7iRGtWY64=
|
||||
github.com/oschwald/maxminddb-golang v1.5.0/go.mod h1:3jhIUymTJ5VREKyIhWm66LJiQt04F0UCDdodShpjWsY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo=
|
||||
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191011234655-491137f69257 h1:ry8e2D+cwaV6hk7lb3aRTjjZo24shrbK0e11QEOkTIg=
|
||||
golang.org/x/net v0.0.0-20191011234655-491137f69257/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/eapache/channels.v1 v1.1.0 h1:5bGAyKKvyCTWjSj7mhefG6Lc68VyN4MH1v8/7OoeeB4=
|
||||
|
@ -2,6 +2,7 @@ package executor
|
||||
|
||||
import (
|
||||
"github.com/Dreamacro/clash/component/auth"
|
||||
trie "github.com/Dreamacro/clash/component/domain-trie"
|
||||
"github.com/Dreamacro/clash/config"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/dns"
|
||||
@ -30,6 +31,7 @@ func ApplyConfig(cfg *config.Config, force bool) {
|
||||
updateProxies(cfg.Proxies)
|
||||
updateRules(cfg.Rules)
|
||||
updateDNS(cfg.DNS)
|
||||
updateHosts(cfg.Hosts)
|
||||
updateExperimental(cfg.Experimental)
|
||||
}
|
||||
|
||||
@ -46,6 +48,7 @@ func GetGeneral() *config.General {
|
||||
RedirPort: ports.RedirPort,
|
||||
Authentication: authenticator,
|
||||
AllowLan: P.AllowLan(),
|
||||
BindAddress: P.BindAddress(),
|
||||
Mode: T.Instance().Mode(),
|
||||
LogLevel: log.Level(),
|
||||
}
|
||||
@ -69,6 +72,10 @@ func updateDNS(c *config.DNS) {
|
||||
IPv6: c.IPv6,
|
||||
EnhancedMode: c.EnhancedMode,
|
||||
Pool: c.FakeIPRange,
|
||||
FallbackFilter: dns.FallbackFilter{
|
||||
GeoIP: c.FallbackFilter.GeoIP,
|
||||
IPCIDR: c.FallbackFilter.IPCIDR,
|
||||
},
|
||||
})
|
||||
dns.DefaultResolver = r
|
||||
if err := dns.ReCreateServer(c.Listen, r); err != nil {
|
||||
@ -81,6 +88,10 @@ func updateDNS(c *config.DNS) {
|
||||
}
|
||||
}
|
||||
|
||||
func updateHosts(tree *trie.Trie) {
|
||||
dns.DefaultHosts = tree
|
||||
}
|
||||
|
||||
func updateProxies(proxies map[string]C.Proxy) {
|
||||
tunnel := T.Instance()
|
||||
oldProxies := tunnel.Proxies()
|
||||
@ -102,9 +113,11 @@ func updateGeneral(general *config.General) {
|
||||
T.Instance().SetMode(general.Mode)
|
||||
|
||||
allowLan := general.AllowLan
|
||||
|
||||
P.SetAllowLan(allowLan)
|
||||
|
||||
bindAddress := general.BindAddress
|
||||
P.SetBindAddress(bindAddress)
|
||||
|
||||
if err := P.ReCreateHTTP(general.Port); err != nil {
|
||||
log.Errorln("Start HTTP server error: %s", err.Error())
|
||||
}
|
||||
|
@ -22,12 +22,13 @@ func configRouter() http.Handler {
|
||||
}
|
||||
|
||||
type configSchema struct {
|
||||
Port *int `json:"port"`
|
||||
SocksPort *int `json:"socks-port"`
|
||||
RedirPort *int `json:"redir-port"`
|
||||
AllowLan *bool `json:"allow-lan"`
|
||||
Mode *T.Mode `json:"mode"`
|
||||
LogLevel *log.LogLevel `json:"log-level"`
|
||||
Port *int `json:"port"`
|
||||
SocksPort *int `json:"socks-port"`
|
||||
RedirPort *int `json:"redir-port"`
|
||||
AllowLan *bool `json:"allow-lan"`
|
||||
BindAddress *string `json:"bind-address"`
|
||||
Mode *T.Mode `json:"mode"`
|
||||
LogLevel *log.LogLevel `json:"log-level"`
|
||||
}
|
||||
|
||||
func getConfigs(w http.ResponseWriter, r *http.Request) {
|
||||
@ -55,6 +56,10 @@ func patchConfigs(w http.ResponseWriter, r *http.Request) {
|
||||
P.SetAllowLan(*general.AllowLan)
|
||||
}
|
||||
|
||||
if general.BindAddress != nil {
|
||||
P.SetBindAddress(*general.BindAddress)
|
||||
}
|
||||
|
||||
ports := P.GetPorts()
|
||||
P.ReCreateHTTP(pointerOrDefault(general.Port, ports.Port))
|
||||
P.ReCreateSocks(pointerOrDefault(general.SocksPort, ports.SocksPort))
|
||||
|
@ -110,27 +110,23 @@ func getProxyDelay(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
proxy := r.Context().Value(CtxKeyProxy).(C.Proxy)
|
||||
|
||||
sigCh := make(chan uint16)
|
||||
go func() {
|
||||
t, err := proxy.URLTest(url)
|
||||
if err != nil {
|
||||
sigCh <- 0
|
||||
}
|
||||
sigCh <- t
|
||||
}()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout))
|
||||
defer cancel()
|
||||
|
||||
select {
|
||||
case <-time.After(time.Millisecond * time.Duration(timeout)):
|
||||
render.Status(r, http.StatusRequestTimeout)
|
||||
delay, err := proxy.URLTest(ctx, url)
|
||||
if ctx.Err() != nil {
|
||||
render.Status(r, http.StatusGatewayTimeout)
|
||||
render.JSON(w, r, ErrRequestTimeout)
|
||||
case t := <-sigCh:
|
||||
if t == 0 {
|
||||
render.Status(r, http.StatusServiceUnavailable)
|
||||
render.JSON(w, r, newError("An error occurred in the delay test"))
|
||||
} else {
|
||||
render.JSON(w, r, render.M{
|
||||
"delay": t,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil || delay == 0 {
|
||||
render.Status(r, http.StatusServiceUnavailable)
|
||||
render.JSON(w, r, newError("An error occurred in the delay test"))
|
||||
return
|
||||
}
|
||||
|
||||
render.JSON(w, r, render.M{
|
||||
"delay": delay,
|
||||
})
|
||||
}
|
||||
|
@ -1,17 +1,20 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
T "github.com/Dreamacro/clash/tunnel"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -19,6 +22,12 @@ var (
|
||||
serverAddr = ""
|
||||
|
||||
uiPath = ""
|
||||
|
||||
upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type Traffic struct {
|
||||
@ -47,15 +56,14 @@ func Start(addr string, secret string) {
|
||||
MaxAge: 300,
|
||||
})
|
||||
|
||||
root := chi.NewRouter().With(jsonContentType)
|
||||
root.Get("/traffic", traffic)
|
||||
root.Get("/logs", getLogs)
|
||||
|
||||
r.Use(cors.Handler)
|
||||
r.Get("/", hello)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(cors.Handler, authentication)
|
||||
r.Use(authentication)
|
||||
|
||||
r.Mount("/", root)
|
||||
r.Get("/logs", getLogs)
|
||||
r.Get("/traffic", traffic)
|
||||
r.Get("/version", version)
|
||||
r.Mount("/configs", configRouter())
|
||||
r.Mount("/proxies", proxyRouter())
|
||||
r.Mount("/rules", ruleRouter())
|
||||
@ -64,7 +72,7 @@ func Start(addr string, secret string) {
|
||||
if uiPath != "" {
|
||||
r.Group(func(r chi.Router) {
|
||||
fs := http.StripPrefix("/ui", http.FileServer(http.Dir(uiPath)))
|
||||
r.Get("/ui", http.RedirectHandler("/ui/", 301).ServeHTTP)
|
||||
r.Get("/ui", http.RedirectHandler("/ui/", http.StatusTemporaryRedirect).ServeHTTP)
|
||||
r.Get("/ui/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
fs.ServeHTTP(w, r)
|
||||
})
|
||||
@ -78,24 +86,28 @@ func Start(addr string, secret string) {
|
||||
}
|
||||
}
|
||||
|
||||
func jsonContentType(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
func authentication(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
header := r.Header.Get("Authorization")
|
||||
text := strings.SplitN(header, " ", 2)
|
||||
|
||||
if serverSecret == "" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Browser websocket not support custom header
|
||||
if websocket.IsWebSocketUpgrade(r) && r.URL.Query().Get("token") != "" {
|
||||
token := r.URL.Query().Get("token")
|
||||
if token != serverSecret {
|
||||
render.Status(r, http.StatusUnauthorized)
|
||||
render.JSON(w, r, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
header := r.Header.Get("Authorization")
|
||||
text := strings.SplitN(header, " ", 2)
|
||||
|
||||
hasUnvalidHeader := text[0] != "Bearer"
|
||||
hasUnvalidSecret := len(text) == 2 && text[1] != serverSecret
|
||||
if hasUnvalidHeader || hasUnvalidSecret {
|
||||
@ -113,19 +125,44 @@ func hello(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func traffic(w http.ResponseWriter, r *http.Request) {
|
||||
render.Status(r, http.StatusOK)
|
||||
var wsConn *websocket.Conn
|
||||
if websocket.IsWebSocketUpgrade(r) {
|
||||
var err error
|
||||
wsConn, err = upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if wsConn == nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
render.Status(r, http.StatusOK)
|
||||
}
|
||||
|
||||
tick := time.NewTicker(time.Second)
|
||||
t := T.Instance().Traffic()
|
||||
buf := &bytes.Buffer{}
|
||||
var err error
|
||||
for range tick.C {
|
||||
buf.Reset()
|
||||
up, down := t.Now()
|
||||
if err := json.NewEncoder(w).Encode(Traffic{
|
||||
if err := json.NewEncoder(buf).Encode(Traffic{
|
||||
Up: up,
|
||||
Down: down,
|
||||
}); err != nil {
|
||||
break
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
|
||||
if wsConn == nil {
|
||||
_, err = w.Write(buf.Bytes())
|
||||
w.(http.Flusher).Flush()
|
||||
} else {
|
||||
err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,20 +184,51 @@ func getLogs(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var wsConn *websocket.Conn
|
||||
if websocket.IsWebSocketUpgrade(r) {
|
||||
var err error
|
||||
wsConn, err = upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if wsConn == nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
render.Status(r, http.StatusOK)
|
||||
}
|
||||
|
||||
sub := log.Subscribe()
|
||||
render.Status(r, http.StatusOK)
|
||||
defer log.UnSubscribe(sub)
|
||||
buf := &bytes.Buffer{}
|
||||
var err error
|
||||
for elm := range sub {
|
||||
buf.Reset()
|
||||
log := elm.(*log.Event)
|
||||
if log.LogLevel < level {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(Log{
|
||||
if err := json.NewEncoder(buf).Encode(Log{
|
||||
Type: log.Type(),
|
||||
Payload: log.Payload,
|
||||
}); err != nil {
|
||||
break
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
|
||||
if wsConn == nil {
|
||||
_, err = w.Write(buf.Bytes())
|
||||
w.(http.Flusher).Flush()
|
||||
} else {
|
||||
err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func version(w http.ResponseWriter, r *http.Request) {
|
||||
render.JSON(w, r, render.M{"version": C.Version})
|
||||
}
|
||||
|
@ -62,6 +62,11 @@ func Subscribe() observable.Subscription {
|
||||
return sub
|
||||
}
|
||||
|
||||
func UnSubscribe(sub observable.Subscription) {
|
||||
source.UnSubscribe(sub)
|
||||
return
|
||||
}
|
||||
|
||||
func Level() LogLevel {
|
||||
return level
|
||||
}
|
||||
|
3
main.go
3
main.go
@ -33,9 +33,6 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
// enable tls 1.3 and remove when go 1.13
|
||||
os.Setenv("GODEBUG", os.Getenv("GODEBUG")+",tls13=1")
|
||||
|
||||
if homedir != "" {
|
||||
if !filepath.IsAbs(homedir) {
|
||||
currentDir, _ := os.Getwd()
|
||||
|
@ -11,11 +11,13 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
allowLan = false
|
||||
allowLan = false
|
||||
bindAddress = "*"
|
||||
|
||||
socksListener *socks.SockListener
|
||||
httpListener *http.HttpListener
|
||||
redirListener *redir.RedirListener
|
||||
socksListener *socks.SockListener
|
||||
socksUDPListener *socks.SockUDPListener
|
||||
httpListener *http.HttpListener
|
||||
redirListener *redir.RedirListener
|
||||
)
|
||||
|
||||
type listener interface {
|
||||
@ -33,12 +35,20 @@ func AllowLan() bool {
|
||||
return allowLan
|
||||
}
|
||||
|
||||
func BindAddress() string {
|
||||
return bindAddress
|
||||
}
|
||||
|
||||
func SetAllowLan(al bool) {
|
||||
allowLan = al
|
||||
}
|
||||
|
||||
func SetBindAddress(host string) {
|
||||
bindAddress = host
|
||||
}
|
||||
|
||||
func ReCreateHTTP(port int) error {
|
||||
addr := genAddr(port, allowLan)
|
||||
addr := genAddr(bindAddress, port, allowLan)
|
||||
|
||||
if httpListener != nil {
|
||||
if httpListener.Address() == addr {
|
||||
@ -62,7 +72,7 @@ func ReCreateHTTP(port int) error {
|
||||
}
|
||||
|
||||
func ReCreateSocks(port int) error {
|
||||
addr := genAddr(port, allowLan)
|
||||
addr := genAddr(bindAddress, port, allowLan)
|
||||
|
||||
if socksListener != nil {
|
||||
if socksListener.Address() == addr {
|
||||
@ -82,11 +92,29 @@ func ReCreateSocks(port int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return reCreateSocksUDP(addr)
|
||||
}
|
||||
|
||||
func reCreateSocksUDP(addr string) error {
|
||||
if socksUDPListener != nil {
|
||||
if socksUDPListener.Address() == addr {
|
||||
return nil
|
||||
}
|
||||
socksUDPListener.Close()
|
||||
socksUDPListener = nil
|
||||
}
|
||||
|
||||
var err error
|
||||
socksUDPListener, err = socks.NewSocksUDPProxy(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReCreateRedir(port int) error {
|
||||
addr := genAddr(port, allowLan)
|
||||
addr := genAddr(bindAddress, port, allowLan)
|
||||
|
||||
if redirListener != nil {
|
||||
if redirListener.Address() == addr {
|
||||
@ -142,9 +170,14 @@ func portIsZero(addr string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func genAddr(port int, allowLan bool) string {
|
||||
func genAddr(host string, port int, allowLan bool) string {
|
||||
if allowLan {
|
||||
return fmt.Sprintf(":%d", port)
|
||||
if host == "*" {
|
||||
return fmt.Sprintf(":%d", port)
|
||||
} else {
|
||||
return fmt.Sprintf("%s:%d", host, port)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("127.0.0.1:%d", port)
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package socks
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
|
||||
adapters "github.com/Dreamacro/clash/adapters/inbound"
|
||||
@ -62,7 +64,8 @@ func handleSocks(conn net.Conn) {
|
||||
}
|
||||
conn.(*net.TCPConn).SetKeepAlive(true)
|
||||
if command == socks5.CmdUDPAssociate {
|
||||
tun.Add(adapters.NewSocket(target, conn, C.SOCKS, C.UDP))
|
||||
defer conn.Close()
|
||||
io.Copy(ioutil.Discard, conn)
|
||||
return
|
||||
}
|
||||
tun.Add(adapters.NewSocket(target, conn, C.SOCKS, C.TCP))
|
||||
|
68
proxy/socks/udp.go
Normal file
68
proxy/socks/udp.go
Normal file
@ -0,0 +1,68 @@
|
||||
package socks
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
|
||||
adapters "github.com/Dreamacro/clash/adapters/inbound"
|
||||
"github.com/Dreamacro/clash/common/pool"
|
||||
"github.com/Dreamacro/clash/component/socks5"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type SockUDPListener struct {
|
||||
net.PacketConn
|
||||
address string
|
||||
closed bool
|
||||
}
|
||||
|
||||
func NewSocksUDPProxy(addr string) (*SockUDPListener, error) {
|
||||
l, err := net.ListenPacket("udp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sl := &SockUDPListener{l, addr, false}
|
||||
go func() {
|
||||
for {
|
||||
buf := pool.BufPool.Get().([]byte)
|
||||
n, remoteAddr, err := l.ReadFrom(buf)
|
||||
if err != nil {
|
||||
pool.BufPool.Put(buf[:cap(buf)])
|
||||
if sl.closed {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
handleSocksUDP(l, buf[:n], remoteAddr)
|
||||
}
|
||||
}()
|
||||
|
||||
return sl, nil
|
||||
}
|
||||
|
||||
func (l *SockUDPListener) Close() error {
|
||||
l.closed = true
|
||||
return l.PacketConn.Close()
|
||||
}
|
||||
|
||||
func (l *SockUDPListener) Address() string {
|
||||
return l.address
|
||||
}
|
||||
|
||||
func handleSocksUDP(pc net.PacketConn, buf []byte, addr net.Addr) {
|
||||
target, payload, err := socks5.DecodeUDPPacket(buf)
|
||||
if err != nil {
|
||||
// Unresolved UDP packet, return buffer to the pool
|
||||
pool.BufPool.Put(buf[:cap(buf)])
|
||||
return
|
||||
}
|
||||
conn := &fakeConn{
|
||||
PacketConn: pc,
|
||||
remoteAddr: addr,
|
||||
targetAddr: target,
|
||||
buffer: bytes.NewBuffer(payload),
|
||||
bufRef: buf,
|
||||
}
|
||||
tun.Add(adapters.NewSocket(target, conn, C.SOCKS, C.UDP))
|
||||
}
|
39
proxy/socks/utils.go
Normal file
39
proxy/socks/utils.go
Normal file
@ -0,0 +1,39 @@
|
||||
package socks
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
|
||||
"github.com/Dreamacro/clash/common/pool"
|
||||
"github.com/Dreamacro/clash/component/socks5"
|
||||
)
|
||||
|
||||
type fakeConn struct {
|
||||
net.PacketConn
|
||||
remoteAddr net.Addr
|
||||
targetAddr socks5.Addr
|
||||
buffer *bytes.Buffer
|
||||
bufRef []byte
|
||||
}
|
||||
|
||||
func (c *fakeConn) Read(b []byte) (n int, err error) {
|
||||
return c.buffer.Read(b)
|
||||
}
|
||||
|
||||
func (c *fakeConn) Write(b []byte) (n int, err error) {
|
||||
packet, err := socks5.EncodeUDPPacket(c.targetAddr, b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return c.PacketConn.WriteTo(packet, c.remoteAddr)
|
||||
}
|
||||
|
||||
func (c *fakeConn) RemoteAddr() net.Addr {
|
||||
return c.remoteAddr
|
||||
}
|
||||
|
||||
func (c *fakeConn) Close() error {
|
||||
err := c.PacketConn.Close()
|
||||
pool.BufPool.Put(c.bufRef[:cap(c.bufRef)])
|
||||
return err
|
||||
}
|
@ -17,6 +17,9 @@ func (t *Tunnel) handleHTTP(request *adapters.HTTPAdapter, outbound net.Conn) {
|
||||
req := request.R
|
||||
host := req.Host
|
||||
|
||||
inboundReeder := bufio.NewReader(request)
|
||||
outboundReeder := bufio.NewReader(conn)
|
||||
|
||||
for {
|
||||
keepAlive := strings.TrimSpace(strings.ToLower(req.Header.Get("Proxy-Connection"))) == "keep-alive"
|
||||
|
||||
@ -27,13 +30,23 @@ func (t *Tunnel) handleHTTP(request *adapters.HTTPAdapter, outbound net.Conn) {
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
br := bufio.NewReader(conn)
|
||||
resp, err := http.ReadResponse(br, req)
|
||||
|
||||
handleResponse:
|
||||
resp, err := http.ReadResponse(outboundReeder, req)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
adapters.RemoveHopByHopHeaders(resp.Header)
|
||||
if resp.ContentLength >= 0 {
|
||||
|
||||
if resp.StatusCode == http.StatusContinue {
|
||||
err = resp.Write(request)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
goto handleResponse
|
||||
}
|
||||
|
||||
if keepAlive || resp.ContentLength >= 0 {
|
||||
resp.Header.Set("Proxy-Connection", "keep-alive")
|
||||
resp.Header.Set("Connection", "keep-alive")
|
||||
resp.Header.Set("Keep-Alive", "timeout=4")
|
||||
@ -46,11 +59,7 @@ func (t *Tunnel) handleHTTP(request *adapters.HTTPAdapter, outbound net.Conn) {
|
||||
break
|
||||
}
|
||||
|
||||
if !keepAlive {
|
||||
break
|
||||
}
|
||||
|
||||
req, err = http.ReadRequest(bufio.NewReader(request))
|
||||
req, err = http.ReadRequest(inboundReeder)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
@ -63,54 +72,46 @@ func (t *Tunnel) handleHTTP(request *adapters.HTTPAdapter, outbound net.Conn) {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tunnel) handleUDPToRemote(conn net.Conn, pc net.PacketConn, addr net.Addr) {
|
||||
buf := pool.BufPool.Get().([]byte)
|
||||
defer pool.BufPool.Put(buf[:cap(buf)])
|
||||
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = pc.WriteTo(buf[:n], addr); err != nil {
|
||||
return
|
||||
}
|
||||
t.traffic.Up() <- int64(n)
|
||||
}
|
||||
|
||||
func (t *Tunnel) handleUDPToLocal(conn net.Conn, pc net.PacketConn, key string, timeout time.Duration) {
|
||||
buf := pool.BufPool.Get().([]byte)
|
||||
defer pool.BufPool.Put(buf[:cap(buf)])
|
||||
defer t.natTable.Delete(key)
|
||||
defer pc.Close()
|
||||
|
||||
for {
|
||||
pc.SetReadDeadline(time.Now().Add(timeout))
|
||||
n, _, err := pc.ReadFrom(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
n, err = conn.Write(buf[:n])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.traffic.Down() <- int64(n)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tunnel) handleSocket(request *adapters.SocketAdapter, outbound net.Conn) {
|
||||
conn := newTrafficTrack(outbound, t.traffic)
|
||||
relay(request, conn)
|
||||
}
|
||||
|
||||
func (t *Tunnel) handleUDPOverTCP(conn net.Conn, pc net.PacketConn, addr net.Addr) error {
|
||||
ch := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
buf := pool.BufPool.Get().([]byte)
|
||||
defer pool.BufPool.Put(buf)
|
||||
for {
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
pc.SetReadDeadline(time.Now().Add(120 * time.Second))
|
||||
if _, err = pc.WriteTo(buf[:n], addr); err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
t.traffic.Up() <- int64(n)
|
||||
ch <- nil
|
||||
}
|
||||
}()
|
||||
|
||||
buf := pool.BufPool.Get().([]byte)
|
||||
defer pool.BufPool.Put(buf)
|
||||
|
||||
for {
|
||||
pc.SetReadDeadline(time.Now().Add(120 * time.Second))
|
||||
n, _, err := pc.ReadFrom(buf)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if _, err := conn.Write(buf[:n]); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
t.traffic.Down() <- int64(n)
|
||||
}
|
||||
|
||||
<-ch
|
||||
return nil
|
||||
}
|
||||
|
||||
// relay copies between left and right bidirectionally.
|
||||
func relay(leftConn, rightConn net.Conn) {
|
||||
ch := make(chan error)
|
||||
|
@ -60,6 +60,6 @@ func (m Mode) String() string {
|
||||
case Direct:
|
||||
return "Direct"
|
||||
default:
|
||||
return "Unknow"
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
167
tunnel/tunnel.go
167
tunnel/tunnel.go
@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
InboundAdapter "github.com/Dreamacro/clash/adapters/inbound"
|
||||
"github.com/Dreamacro/clash/component/nat"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/dns"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
@ -17,11 +18,16 @@ import (
|
||||
var (
|
||||
tunnel *Tunnel
|
||||
once sync.Once
|
||||
|
||||
// default timeout for UDP session
|
||||
udpTimeout = 60 * time.Second
|
||||
)
|
||||
|
||||
// Tunnel handle relay inbound proxy and outbound proxy
|
||||
type Tunnel struct {
|
||||
queue *channels.InfiniteChannel
|
||||
tcpQueue *channels.InfiniteChannel
|
||||
udpQueue *channels.InfiniteChannel
|
||||
natTable *nat.Table
|
||||
rules []C.Rule
|
||||
proxies map[string]C.Proxy
|
||||
configMux *sync.RWMutex
|
||||
@ -36,7 +42,12 @@ type Tunnel struct {
|
||||
|
||||
// Add request to queue
|
||||
func (t *Tunnel) Add(req C.ServerAdapter) {
|
||||
t.queue.In() <- req
|
||||
switch req.Metadata().NetWork {
|
||||
case C.TCP:
|
||||
t.tcpQueue.In() <- req
|
||||
case C.UDP:
|
||||
t.udpQueue.In() <- req
|
||||
}
|
||||
}
|
||||
|
||||
// Traffic return traffic of all connections
|
||||
@ -86,11 +97,18 @@ func (t *Tunnel) SetMode(mode Mode) {
|
||||
}
|
||||
|
||||
func (t *Tunnel) process() {
|
||||
queue := t.queue.Out()
|
||||
for {
|
||||
elm := <-queue
|
||||
go func() {
|
||||
queue := t.udpQueue.Out()
|
||||
for elm := range queue {
|
||||
conn := elm.(C.ServerAdapter)
|
||||
t.handleUDPConn(conn)
|
||||
}
|
||||
}()
|
||||
|
||||
queue := t.tcpQueue.Out()
|
||||
for elm := range queue {
|
||||
conn := elm.(C.ServerAdapter)
|
||||
go t.handleConn(conn)
|
||||
go t.handleTCPConn(conn)
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,15 +120,7 @@ func (t *Tunnel) needLookupIP(metadata *C.Metadata) bool {
|
||||
return dns.DefaultResolver != nil && (dns.DefaultResolver.IsMapping() || dns.DefaultResolver.IsFakeIP()) && metadata.Host == "" && metadata.DstIP != nil
|
||||
}
|
||||
|
||||
func (t *Tunnel) handleConn(localConn C.ServerAdapter) {
|
||||
defer localConn.Close()
|
||||
metadata := localConn.Metadata()
|
||||
|
||||
if !metadata.Valid() {
|
||||
log.Warnln("[Metadata] not valid: %#v", metadata)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *Tunnel) resolveMetadata(metadata *C.Metadata) (C.Proxy, C.Rule, error) {
|
||||
// preprocess enhanced-mode metadata
|
||||
if t.needLookupIP(metadata) {
|
||||
host, exist := dns.DefaultResolver.IPToHost(*metadata.DstIP)
|
||||
@ -124,6 +134,7 @@ func (t *Tunnel) handleConn(localConn C.ServerAdapter) {
|
||||
}
|
||||
|
||||
var proxy C.Proxy
|
||||
var rule C.Rule
|
||||
switch t.mode {
|
||||
case Direct:
|
||||
proxy = t.proxies["DIRECT"]
|
||||
@ -132,36 +143,107 @@ func (t *Tunnel) handleConn(localConn C.ServerAdapter) {
|
||||
// Rule
|
||||
default:
|
||||
var err error
|
||||
proxy, err = t.match(metadata)
|
||||
proxy, rule, err = t.match(metadata)
|
||||
if err != nil {
|
||||
return
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
return proxy, rule, nil
|
||||
}
|
||||
|
||||
if metadata.NetWork == C.UDP {
|
||||
pc, addr, err := proxy.DialUDP(metadata)
|
||||
if err != nil {
|
||||
log.Warnln("Proxy[%s] connect [%s --> %s] error: %s", proxy.Name(), metadata.SrcIP.String(), metadata.String(), err.Error())
|
||||
return
|
||||
}
|
||||
defer pc.Close()
|
||||
|
||||
t.handleUDPOverTCP(localConn, pc, addr)
|
||||
func (t *Tunnel) handleUDPConn(localConn C.ServerAdapter) {
|
||||
metadata := localConn.Metadata()
|
||||
if !metadata.Valid() {
|
||||
log.Warnln("[Metadata] not valid: %#v", metadata)
|
||||
return
|
||||
}
|
||||
|
||||
remoConn, err := proxy.Dial(metadata)
|
||||
src := localConn.RemoteAddr().String()
|
||||
dst := metadata.RemoteAddress()
|
||||
key := src + "-" + dst
|
||||
|
||||
pc, addr := t.natTable.Get(key)
|
||||
if pc != nil {
|
||||
t.handleUDPToRemote(localConn, pc, addr)
|
||||
return
|
||||
}
|
||||
|
||||
lockKey := key + "-lock"
|
||||
wg, loaded := t.natTable.GetOrCreateLock(lockKey)
|
||||
go func() {
|
||||
if !loaded {
|
||||
wg.Add(1)
|
||||
proxy, rule, err := t.resolveMetadata(metadata)
|
||||
if err != nil {
|
||||
log.Warnln("Parse metadata failed: %s", err.Error())
|
||||
t.natTable.Delete(lockKey)
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
|
||||
rawPc, nAddr, err := proxy.DialUDP(metadata)
|
||||
if err != nil {
|
||||
log.Warnln("dial %s error: %s", proxy.Name(), err.Error())
|
||||
t.natTable.Delete(lockKey)
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
pc = rawPc
|
||||
addr = nAddr
|
||||
|
||||
if rule != nil {
|
||||
log.Infoln("%s --> %v match %s using %s", metadata.SrcIP.String(), metadata.String(), rule.RuleType().String(), rawPc.Chains().String())
|
||||
} else {
|
||||
log.Infoln("%s --> %v doesn't match any rule using DIRECT", metadata.SrcIP.String(), metadata.String())
|
||||
}
|
||||
|
||||
t.natTable.Set(key, pc, addr)
|
||||
t.natTable.Delete(lockKey)
|
||||
wg.Done()
|
||||
go t.handleUDPToLocal(localConn, pc, key, udpTimeout)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
pc, addr := t.natTable.Get(key)
|
||||
if pc != nil {
|
||||
t.handleUDPToRemote(localConn, pc, addr)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (t *Tunnel) handleTCPConn(localConn C.ServerAdapter) {
|
||||
defer localConn.Close()
|
||||
|
||||
metadata := localConn.Metadata()
|
||||
if !metadata.Valid() {
|
||||
log.Warnln("[Metadata] not valid: %#v", metadata)
|
||||
return
|
||||
}
|
||||
|
||||
proxy, rule, err := t.resolveMetadata(metadata)
|
||||
if err != nil {
|
||||
log.Warnln("Proxy[%s] connect [%s --> %s] error: %s", proxy.Name(), metadata.SrcIP.String(), metadata.String(), err.Error())
|
||||
log.Warnln("Parse metadata failed: %v", err)
|
||||
return
|
||||
}
|
||||
defer remoConn.Close()
|
||||
|
||||
remoteConn, err := proxy.Dial(metadata)
|
||||
if err != nil {
|
||||
log.Warnln("dial %s error: %s", proxy.Name(), err.Error())
|
||||
return
|
||||
}
|
||||
defer remoteConn.Close()
|
||||
|
||||
if rule != nil {
|
||||
log.Infoln("%s --> %v match %s using %s", metadata.SrcIP.String(), metadata.String(), rule.RuleType().String(), remoteConn.Chains().String())
|
||||
} else {
|
||||
log.Infoln("%s --> %v doesn't match any rule using DIRECT", metadata.SrcIP.String(), metadata.String())
|
||||
}
|
||||
|
||||
switch adapter := localConn.(type) {
|
||||
case *InboundAdapter.HTTPAdapter:
|
||||
t.handleHTTP(adapter, remoConn)
|
||||
t.handleHTTP(adapter, remoteConn)
|
||||
case *InboundAdapter.SocketAdapter:
|
||||
t.handleSocket(adapter, remoConn)
|
||||
t.handleSocket(adapter, remoteConn)
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,17 +251,24 @@ func (t *Tunnel) shouldResolveIP(rule C.Rule, metadata *C.Metadata) bool {
|
||||
return (rule.RuleType() == C.GEOIP || rule.RuleType() == C.IPCIDR) && metadata.Host != "" && metadata.DstIP == nil
|
||||
}
|
||||
|
||||
func (t *Tunnel) match(metadata *C.Metadata) (C.Proxy, error) {
|
||||
func (t *Tunnel) match(metadata *C.Metadata) (C.Proxy, C.Rule, error) {
|
||||
t.configMux.RLock()
|
||||
defer t.configMux.RUnlock()
|
||||
|
||||
var resolved bool
|
||||
|
||||
if node := dns.DefaultHosts.Search(metadata.Host); node != nil {
|
||||
ip := node.Data.(net.IP)
|
||||
metadata.DstIP = &ip
|
||||
resolved = true
|
||||
}
|
||||
|
||||
for _, rule := range t.rules {
|
||||
if !resolved && t.shouldResolveIP(rule, metadata) {
|
||||
ip, err := t.resolveIP(metadata.Host)
|
||||
if err != nil {
|
||||
if !t.ignoreResolveFail {
|
||||
return nil, fmt.Errorf("[DNS] resolve %s error: %s", metadata.Host, err.Error())
|
||||
return nil, nil, fmt.Errorf("[DNS] resolve %s error: %s", metadata.Host, err.Error())
|
||||
}
|
||||
log.Debugln("[DNS] resolve %s error: %s", metadata.Host, err.Error())
|
||||
} else {
|
||||
@ -196,20 +285,20 @@ func (t *Tunnel) match(metadata *C.Metadata) (C.Proxy, error) {
|
||||
}
|
||||
|
||||
if metadata.NetWork == C.UDP && !adapter.SupportUDP() {
|
||||
log.Debugln("%v UDP is not supported", adapter.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
log.Infoln("%s --> %v match %s using %s", metadata.SrcIP.String(), metadata.String(), rule.RuleType().String(), rule.Adapter())
|
||||
return adapter, nil
|
||||
return adapter, rule, nil
|
||||
}
|
||||
}
|
||||
log.Infoln("%s --> %v doesn't match any rule using DIRECT", metadata.SrcIP.String(), metadata.String())
|
||||
return t.proxies["DIRECT"], nil
|
||||
return t.proxies["DIRECT"], nil, nil
|
||||
}
|
||||
|
||||
func newTunnel() *Tunnel {
|
||||
return &Tunnel{
|
||||
queue: channels.NewInfiniteChannel(),
|
||||
tcpQueue: channels.NewInfiniteChannel(),
|
||||
udpQueue: channels.NewInfiniteChannel(),
|
||||
natTable: nat.New(),
|
||||
proxies: make(map[string]C.Proxy),
|
||||
configMux: &sync.RWMutex{},
|
||||
traffic: C.NewTraffic(time.Second),
|
||||
|
Reference in New Issue
Block a user