Merge pull request #1 from Dreamacro/master

更新
This commit is contained in:
Clash-Mini 2021-11-09 16:14:04 +08:00 committed by GitHub
commit cec14db4a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
232 changed files with 7999 additions and 2568 deletions

View File

@ -1,100 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: "[Bug]"
labels: ''
assignees: ''
---
<!--
感谢你向 Clash Core 提交 issue
在提交之前,请确认:
- [ ] 如果你可以自己 debug 并解决的话,提交 PR 吧!
- [ ] 我已经在 [Issue Tracker](……/) 中找过我要提出的问题
- [ ] 我已经使用 dev 分支版本测试过,问题依旧存在
- [ ] 我已经仔细看过 [Documentation](https://github.com/Dreamacro/clash/wiki/) 并无法自行解决问题
- [ ] 这是 Clash 核心的问题,并非我所使用的 Clash 衍生版本(如 OpenClash、KoolClash 等)的特定问题
请注意,如果你并没有遵照这个 issue template 填写内容,我们将直接关闭这个 issue。
Thanks for opening an issue towards the Clash core!
But before so, please do the following checklist:
- [ ] Is this something you can **debug and fix**? Send a pull request! Bug fixes and documentation fixes are welcome.
- [ ] I have searched on the [issue tracker](……/) for a related issue.
- [ ] I have tested using the dev branch, and the issue still exists.
- [ ] I have read the [documentation](https://github.com/Dreamacro/clash/wiki/) and was unable to solve the issue
- [ ] This is an issue of the Clash core *per se*, not to the derivatives of Clash, like OpenClash or KoolClash
Please understand that we close issues that fail to follow this issue template.
-->
------------------------------------------------------------------
<!--
请附上任何可以帮助我们解决这个问题的信息,如果我们收到的信息不足,我们将对这个 issue 加上 *Needs more information* 标记并在收到更多资讯之前关闭 issue。
Make sure to add **all the information needed to understand the bug** so that someone can help. If the info is missing we'll add the 'Needs more information' label and close the issue until there is enough information.
-->
### Clash config
<!--
在下方附上 Clash core 脱敏后配置文件的内容
Paste the Clash core configuration below.
-->
<details>
<summary>config.yaml</summary>
```yaml
……
```
</details>
### Clash log
<!--
在下方附上 Clash Core 的日志log level 使用 DEBUG
Paste the Clash core log below with the log level set to `DEBUG`.
-->
```
……
```
### 环境 Environment
* 操作系统 (the OS that the Clash core is running on)
……
* 网路环境或拓扑 (network conditions/topology)
……
* iptables如果适用 (if applicable)
……
* ISP 有没有进行 DNS 污染 (is your ISP performing DNS pollution?)
……
* 其他 (any other information that would be useful)
……
### 说明 Description
<!--
请详细、清晰地表达你要提出的论述,例如这个问题如何影响到你?你想实现什么功能?
-->
### 重现问题的具体布骤 Steps to Reproduce
1. [First Step]
2. [Second Step]
3. ……
**我预期会发生……?**
<!-- **Expected behavior:** [What you expected to happen] -->
**实际上发生了什么?**
<!-- **Actual behavior:** [What actually happened] -->
### 可能的解决方案 Possible Solution
<!-- 此项非必须,但是如果你有想法的话欢迎提出。 -->
<!-- Not obligatory, but suggest a fix/reason for the bug, -->
<!-- or ideas how to implement the addition or change -->
### 更多信息 More Information

76
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,76 @@
name: Bug report
description: Create a report to help us improve
title: "[Bug] "
body:
- type: checkboxes
id: ensure
attributes:
label: Verify steps
description: "
在提交之前,请确认
Please verify that you've followed these steps
"
options:
- label: "
如果你可以自己 debug 并解决的话,提交 PR 吧
Is this something you can **debug and fix**? Send a pull request! Bug fixes and documentation fixes are welcome.
"
required: true
- label: "
我已经在 [Issue Tracker](……/) 中找过我要提出的问题
I have searched on the [issue tracker](……/) for a related issue.
"
required: true
- label: "
我已经使用 dev 分支版本测试过,问题依旧存在
I have tested using the dev branch, and the issue still exists.
"
required: true
- label: "
我已经仔细看过 [Documentation](https://github.com/Dreamacro/clash/wiki/) 并无法自行解决问题
I have read the [documentation](https://github.com/Dreamacro/clash/wiki/) and was unable to solve the issue.
"
required: true
- label: "
这是 Clash 核心的问题,并非我所使用的 Clash 衍生版本(如 OpenClash、KoolClash 等)的特定问题
This is an issue of the Clash core *per se*, not to the derivatives of Clash, like OpenClash or KoolClash.
"
required: true
- type: input
attributes:
label: Clash version
validations:
required: true
- type: dropdown
id: os
attributes:
label: What OS are you seeing the problem on?
multiple: true
options:
- macOS
- Windows
- Linux
- OpenBSD/FreeBSD
- type: textarea
attributes:
render: yaml
label: "Clash config"
description: "
在下方附上 Clash core 脱敏后配置文件的内容
Paste the Clash core configuration below.
"
validations:
required: true
- type: textarea
attributes:
render: shell
label: Clash log
description: "
在下方附上 Clash Core 的日志log level 使用 DEBUG
Paste the Clash core log below with the log level set to `DEBUG`.
"
- type: textarea
attributes:
label: Description
validations:
required: true

6
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,6 @@
blank_issues_enabled: false
contact_links:
- name: Get help in GitHub Discussions
url: https://github.com/Dreamacro/clash/discussions
about: Have a question? Not sure if your issue affects everyone reproducibly? The quickest way to get help is on Clash's GitHub Discussions!

View File

@ -1,78 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature]"
labels: ''
assignees: ''
---
<!-- The English version is available. -->
感谢你向 Clash Core 提交 Feature Request
在提交之前,请确认:
- [ ] 我已经在 [Issue Tracker](……/) 中找过我要提出的请求
请注意,如果你并没有遵照这个 issue template 填写内容,我们将直接关闭这个 issue。
<!--
Thanks for submitting a feature request towards the Clash core!
But before so, please do the following checklist:
- [ ] I have searched on the [issue tracker](……/) before creating the issue.
Please understand that we close issues that fail to follow the issue template.
-->
我都确认过了,我要继续提交。
<!-- None of the above, create a feature request -->
------------------------------------------------------------------
请附上任何可以帮助我们解决这个问题的信息,如果我们收到的信息不足,我们将对这个 issue 加上 *Needs more information* 标记并在收到更多资讯之前关闭 issue。
<!-- Make sure to add **all the information needed to understand the bug** so that someone can help. If the info is missing we'll add the 'Needs more information' label and close the issue until there is enough information. -->
### Clash core config
<!--
在下方附上 Clash Core 脱敏后的配置内容
Paste the Clash core configuration below.
-->
```
……
```
### Clash log
<!--
在下方附上 Clash Core 的日志log level 请使用 DEBUG
Paste the Clash core log below with the log level set to `DEBUG`.
-->
```
……
```
### 环境 Environment
* Clash Core 的操作系统 (the OS that the Clash core is running on)
……
* 使用者的操作系统 (the OS running on the client)
……
* 网路环境或拓扑 (network conditions/topology)
……
* iptables如果适用 (if applicable)
……
* ISP 有没有进行 DNS 污染 (is your ISP performing DNS pollution?)
……
* 其他
……
### 说明 Description
<!--
请详细、清晰地表达你要提出的论述,例如这个问题如何影响到你?你想实现什么功能?目前 Clash Core 的行为是什麽?
-->
### 可能的解决方案 Possible Solution
<!-- 此项非必须,但是如果你有想法的话欢迎提出。 -->
<!-- Not obligatory, but suggest a fix/reason for the bug, -->
<!-- or ideas how to implement the addition or change -->
### 更多信息 More Information

View File

@ -0,0 +1,36 @@
name: Feature request
description: Suggest an idea for this project
title: "[Feature] "
body:
- type: checkboxes
id: ensure
attributes:
label: Verify steps
description: "
在提交之前,请确认
Please verify that you've followed these steps
"
options:
- label: "
我已经在 [Issue Tracker](……/) 中找过我要提出的请求
I have searched on the [issue tracker](……/) for a related feature request.
"
required: true
- label: "
我已经仔细看过 [Documentation](https://github.com/Dreamacro/clash/wiki/) 并无法自行解决问题
I have read the [documentation](https://github.com/Dreamacro/clash/wiki/) and was unable to solve the issue.
"
required: true
- type: textarea
attributes:
label: Description
description: 请详细、清晰地表达你要提出的论述,例如这个问题如何影响到你?你想实现什么功能?目前 Clash Core 的行为是什麽?
validations:
required: true
- type: textarea
attributes:
label: Possible Solution
description: "
此项非必须,但是如果你有想法的话欢迎提出。
Not obligatory, but suggest a fix/reason for the bug, or ideas how to implement the addition or change
"

View File

@ -52,7 +52,7 @@ jobs:
- name: Get all docker tags - name: Get all docker tags
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
uses: actions/github-script@v3 uses: actions/github-script@v4
id: tags id: tags
with: with:
script: | script: |

12
.github/workflows/linter.yml vendored Normal file
View File

@ -0,0 +1,12 @@
name: Linter
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: latest
args: --disable-all -E govet -E gofumpt -E megacheck ./...

View File

@ -1,15 +1,18 @@
name: Go name: Release
on: [push, pull_request] on: [push]
jobs: jobs:
build: build:
name: Build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Get latest go version
id: version
run: |
echo ::set-output name=go_version::$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g')
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: 1.16 go-version: ${{ steps.version.outputs.go_version }}
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v2 uses: actions/checkout@v2
@ -22,12 +25,9 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-go- ${{ runner.os }}-go-
- name: Get dependencies, run test and static check - name: Get dependencies, run test
run: | run: |
go test ./... go test ./...
go vet ./...
go install honnef.co/go/tools/cmd/staticcheck@latest
staticcheck -- $(go list ./...)
- name: Build - name: Build
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
@ -39,8 +39,6 @@ jobs:
- name: Upload Release - name: Upload Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
files: bin/* files: bin/*
draft: true draft: true

View File

@ -11,9 +11,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v3 - uses: actions/stale@v4
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days' stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days'
days-before-stale: 60 days-before-stale: 60
days-before-close: 5 days-before-close: 5

5
.gitignore vendored
View File

@ -12,7 +12,7 @@ bin/*
# Output of the go coverage tool, specifically when used with LiteIDE # Output of the go coverage tool, specifically when used with LiteIDE
*.out *.out
# dep # go mod vendor
vendor vendor
# GoLand # GoLand
@ -20,3 +20,6 @@ vendor
# macOS file # macOS file
.DS_Store .DS_Store
# test suite
test/config/cache*

View File

@ -28,6 +28,7 @@ PLATFORM_LIST = \
WINDOWS_ARCH_LIST = \ WINDOWS_ARCH_LIST = \
windows-386 \ windows-386 \
windows-amd64 \ windows-amd64 \
windows-arm64 \
windows-arm32v7 windows-arm32v7
all: linux-amd64 darwin-amd64 windows-amd64 # Most used all: linux-amd64 darwin-amd64 windows-amd64 # Most used
@ -91,7 +92,10 @@ windows-386:
windows-amd64: windows-amd64:
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-arm64:
GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-arm32v7: windows-arm32v7:
GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
@ -108,5 +112,9 @@ $(zip_releases): %.zip : %
all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST) all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST)
releases: $(gz_releases) $(zip_releases) releases: $(gz_releases) $(zip_releases)
lint:
golangci-lint run --disable-all -E govet -E gofumpt -E megacheck ./...
clean: clean:
rm $(BINDIR)/* rm $(BINDIR)/*

View File

@ -12,9 +12,13 @@
<a href="https://goreportcard.com/report/github.com/Dreamacro/clash"> <a href="https://goreportcard.com/report/github.com/Dreamacro/clash">
<img src="https://goreportcard.com/badge/github.com/Dreamacro/clash?style=flat-square"> <img src="https://goreportcard.com/badge/github.com/Dreamacro/clash?style=flat-square">
</a> </a>
<img src="https://img.shields.io/github/go-mod/go-version/Dreamacro/clash?style=flat-square">
<a href="https://github.com/Dreamacro/clash/releases"> <a href="https://github.com/Dreamacro/clash/releases">
<img src="https://img.shields.io/github/release/Dreamacro/clash/all.svg?style=flat-square"> <img src="https://img.shields.io/github/release/Dreamacro/clash/all.svg?style=flat-square">
</a> </a>
<a href="https://github.com/Dreamacro/clash/releases/tag/premium">
<img src="https://img.shields.io/badge/release-Premium-00b4f0?style=flat-square">
</a>
</p> </p>
## Features ## Features
@ -22,7 +26,7 @@
- Local HTTP/HTTPS/SOCKS server with authentication support - Local HTTP/HTTPS/SOCKS server with authentication support
- VMess, Shadowsocks, Trojan, Snell protocol support for remote connections - VMess, Shadowsocks, Trojan, Snell protocol support for remote connections
- Built-in DNS server that aims to minimize DNS pollution attack impact, supports DoH/DoT upstream and fake IP. - Built-in DNS server that aims to minimize DNS pollution attack impact, supports DoH/DoT upstream and fake IP.
- Rules based off domains, GEOIP, IP CIDR or ports to forward packets to different nodes - Rules based off domains, GEOIP, IPCIDR or Process to forward packets to different nodes
- Remote groups allow users to implement powerful rules. Supports automatic fallback, load balancing or auto select node based off latency - Remote groups allow users to implement powerful rules. Supports automatic fallback, load balancing or auto select node based off latency
- Remote providers, allowing users to get node lists remotely instead of hardcoding in config - Remote providers, allowing users to get node lists remotely instead of hardcoding in config
- Netfilter TCP redirecting. Deploy Clash on your Internet gateway with `iptables`. - Netfilter TCP redirecting. Deploy Clash on your Internet gateway with `iptables`.
@ -40,20 +44,17 @@ Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash
## Premium Release ## Premium Release
[Release](https://github.com/Dreamacro/clash/releases/tag/premium) [Release](https://github.com/Dreamacro/clash/releases/tag/premium)
## Development
If you want to build an application that uses clash as a library, check out the the [GitHub Wiki](https://github.com/Dreamacro/clash/wiki/use-clash-as-a-library)
## Credits ## Credits
* [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2) * [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
* [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core) * [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
* [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go)
## License ## License
This software is released under the GPL-3.0 license. This software is released under the GPL-3.0 license.
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FDreamacro%2Fclash.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_large) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FDreamacro%2Fclash.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_large)
## TODO
- [x] Complementing the necessary rule operators
- [x] Redir proxy
- [x] UDP support
- [x] Connection manager

View File

@ -1,110 +1,21 @@
package outbound package adapter
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "fmt"
"net" "net"
"net/http" "net/http"
"net/url"
"time" "time"
"github.com/Dreamacro/clash/common/queue" "github.com/Dreamacro/clash/common/queue"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"go.uber.org/atomic" "go.uber.org/atomic"
) )
type Base struct {
name string
addr string
tp C.AdapterType
udp bool
}
// Name implements C.ProxyAdapter
func (b *Base) Name() string {
return b.name
}
// Type implements C.ProxyAdapter
func (b *Base) Type() C.AdapterType {
return b.tp
}
// StreamConn implements C.ProxyAdapter
func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return c, errors.New("no support")
}
// DialUDP implements C.ProxyAdapter
func (b *Base) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
return nil, errors.New("no support")
}
// SupportUDP implements C.ProxyAdapter
func (b *Base) SupportUDP() bool {
return b.udp
}
// MarshalJSON implements C.ProxyAdapter
func (b *Base) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": b.Type().String(),
})
}
// Addr implements C.ProxyAdapter
func (b *Base) Addr() string {
return b.addr
}
// Unwrap implements C.ProxyAdapter
func (b *Base) Unwrap(metadata *C.Metadata) C.Proxy {
return nil
}
func NewBase(name string, addr string, tp C.AdapterType, udp bool) *Base {
return &Base{name, addr, tp, udp}
}
type conn struct {
net.Conn
chain C.Chain
}
// Chains implements C.Connection
func (c *conn) Chains() C.Chain {
return c.chain
}
// AppendToChains implements C.Connection
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
}
// Chains implements C.Connection
func (c *packetConn) Chains() C.Chain {
return c.chain
}
// AppendToChains implements C.Connection
func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name())
}
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
return &packetConn{pc, []string{a.Name()}}
}
type Proxy struct { type Proxy struct {
C.ProxyAdapter C.ProxyAdapter
history *queue.Queue history *queue.Queue
@ -118,20 +29,32 @@ func (p *Proxy) Alive() bool {
// Dial implements C.Proxy // Dial implements C.Proxy
func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) { func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout) ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
defer cancel() defer cancel()
return p.DialContext(ctx, metadata) return p.DialContext(ctx, metadata)
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
conn, err := p.ProxyAdapter.DialContext(ctx, metadata) conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...)
if err != nil { p.alive.Store(err == nil)
p.alive.Store(false)
}
return conn, err return conn, err
} }
// DialUDP implements C.ProxyAdapter
func (p *Proxy) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultUDPTimeout)
defer cancel()
return p.ListenPacketContext(ctx, metadata)
}
// ListenPacketContext implements C.ProxyAdapter
func (p *Proxy) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
p.alive.Store(err == nil)
return pc, err
}
// DelayHistory implements C.Proxy // DelayHistory implements C.Proxy
func (p *Proxy) DelayHistory() []C.DelayHistory { func (p *Proxy) DelayHistory() []C.DelayHistory {
queue := p.history.Copy() queue := p.history.Copy()
@ -172,6 +95,7 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
json.Unmarshal(inner, &mapping) json.Unmarshal(inner, &mapping)
mapping["history"] = p.DelayHistory() mapping["history"] = p.DelayHistory()
mapping["name"] = p.Name() mapping["name"] = p.Name()
mapping["udp"] = p.SupportUDP()
return json.Marshal(mapping) return json.Marshal(mapping)
} }
@ -225,6 +149,8 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
return http.ErrUseLastResponse return http.ErrUseLastResponse
}, },
} }
defer client.CloseIdleConnections()
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return return
@ -237,3 +163,31 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
func NewProxy(adapter C.ProxyAdapter) *Proxy { func NewProxy(adapter C.ProxyAdapter) *Proxy {
return &Proxy{adapter, queue.New(10), atomic.NewBool(true)} return &Proxy{adapter, queue.New(10), atomic.NewBool(true)}
} }
func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
u, err := url.Parse(rawURL)
if err != nil {
return
}
port := u.Port()
if port == "" {
switch u.Scheme {
case "https":
port = "443"
case "http":
port = "80"
default:
err = fmt.Errorf("%s scheme not Support", rawURL)
return
}
}
addr = C.Metadata{
AddrType: C.AtypDomainName,
Host: u.Hostname(),
DstIP: nil,
DstPort: port,
}
return
}

21
adapter/inbound/http.go Normal file
View File

@ -0,0 +1,21 @@
package inbound
import (
"net"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
"github.com/Dreamacro/clash/transport/socks5"
)
// NewHTTP receive normal http request and return HTTPContext
func NewHTTP(target socks5.Addr, source net.Addr, conn net.Conn) *context.ConnContext {
metadata := parseSocksAddr(target)
metadata.NetWork = C.TCP
metadata.Type = C.HTTP
if ip, port, err := parseAddr(source.String()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
}
return context.NewConnContext(conn, metadata)
}

View File

@ -1,8 +1,8 @@
package inbound package inbound
import ( import (
"github.com/Dreamacro/clash/component/socks5"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5"
) )
// PacketAdapter is a UDP Packet adapter for socks/redir/tun // PacketAdapter is a UDP Packet adapter for socks/redir/tun

View File

@ -3,9 +3,9 @@ package inbound
import ( import (
"net" "net"
"github.com/Dreamacro/clash/component/socks5"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context" "github.com/Dreamacro/clash/context"
"github.com/Dreamacro/clash/transport/socks5"
) )
// NewSocket receive TCP inbound and return ConnContext // NewSocket receive TCP inbound and return ConnContext

View File

@ -6,8 +6,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/Dreamacro/clash/component/socks5"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5"
) )
func parseSocksAddr(target socks5.Addr) *C.Metadata { func parseSocksAddr(target socks5.Addr) *C.Metadata {

138
adapter/outbound/base.go Normal file
View File

@ -0,0 +1,138 @@
package outbound
import (
"context"
"encoding/json"
"errors"
"net"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
)
type Base struct {
name string
addr string
iface string
tp C.AdapterType
udp bool
rmark int
}
// Name implements C.ProxyAdapter
func (b *Base) Name() string {
return b.name
}
// Type implements C.ProxyAdapter
func (b *Base) Type() C.AdapterType {
return b.tp
}
// StreamConn implements C.ProxyAdapter
func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return c, errors.New("no support")
}
// ListenPacketContext implements C.ProxyAdapter
func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
return nil, errors.New("no support")
}
// SupportUDP implements C.ProxyAdapter
func (b *Base) SupportUDP() bool {
return b.udp
}
// MarshalJSON implements C.ProxyAdapter
func (b *Base) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": b.Type().String(),
})
}
// Addr implements C.ProxyAdapter
func (b *Base) Addr() string {
return b.addr
}
// Unwrap implements C.ProxyAdapter
func (b *Base) Unwrap(metadata *C.Metadata) C.Proxy {
return nil
}
// DialOptions return []dialer.Option from struct
func (b *Base) DialOptions(opts ...dialer.Option) []dialer.Option {
if b.iface != "" {
opts = append(opts, dialer.WithInterface(b.iface))
}
if b.rmark != 0 {
opts = append(opts, dialer.WithRoutingMark(b.rmark))
}
return opts
}
type BasicOption struct {
Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"`
RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"`
}
type BaseOption struct {
Name string
Addr string
Type C.AdapterType
UDP bool
Interface string
RoutingMark int
}
func NewBase(opt BaseOption) *Base {
return &Base{
name: opt.Name,
addr: opt.Addr,
tp: opt.Type,
udp: opt.UDP,
iface: opt.Interface,
rmark: opt.RoutingMark,
}
}
type conn struct {
net.Conn
chain C.Chain
}
// Chains implements C.Connection
func (c *conn) Chains() C.Chain {
return c.chain
}
// AppendToChains implements C.Connection
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
}
// Chains implements C.Connection
func (c *packetConn) Chains() C.Chain {
return c.chain
}
// AppendToChains implements C.Connection
func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name())
}
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
return &packetConn{pc, []string{a.Name()}}
}

View File

@ -13,10 +13,8 @@ type Direct struct {
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
address := net.JoinHostPort(metadata.String(), metadata.DstPort) c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...)
c, err := dialer.DialContext(ctx, "tcp", address)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -24,9 +22,9 @@ func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn,
return NewConn(c, d), nil return NewConn(c, d), nil
} }
// DialUDP implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (d *Direct) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := dialer.ListenPacket("udp", "") pc, err := dialer.ListenPacket(ctx, "udp", "", d.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -25,6 +25,7 @@ type Http struct {
} }
type HttpOption struct { type HttpOption struct {
BasicOption
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port"` Port int `proxy:"port"`
@ -53,8 +54,8 @@ func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", h.addr) c, err := dialer.DialContext(ctx, "tcp", h.addr, h.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", h.addr, err) return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
} }
@ -125,16 +126,16 @@ func NewHttp(option HttpOption) *Http {
} }
tlsConfig = &tls.Config{ tlsConfig = &tls.Config{
InsecureSkipVerify: option.SkipCertVerify, InsecureSkipVerify: option.SkipCertVerify,
ClientSessionCache: getClientSessionCache(),
ServerName: sni, ServerName: sni,
} }
} }
return &Http{ return &Http{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Http, tp: C.Http,
iface: option.Interface,
}, },
user: option.UserName, user: option.UserName,
pass: option.Password, pass: option.Password,

View File

@ -7,6 +7,7 @@ import (
"net" "net"
"time" "time"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
) )
@ -15,12 +16,12 @@ type Reject struct {
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
return NewConn(&NopConn{}, r), nil return NewConn(&NopConn{}, r), nil
} }
// DialUDP implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (r *Reject) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
return nil, errors.New("match reject rule") return nil, errors.New("match reject rule")
} }

View File

@ -2,7 +2,6 @@ package outbound
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net" "net"
@ -10,10 +9,10 @@ import (
"github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/common/structure"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
obfs "github.com/Dreamacro/clash/component/simple-obfs"
"github.com/Dreamacro/clash/component/socks5"
v2rayObfs "github.com/Dreamacro/clash/component/v2ray-plugin"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
obfs "github.com/Dreamacro/clash/transport/simple-obfs"
"github.com/Dreamacro/clash/transport/socks5"
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
"github.com/Dreamacro/go-shadowsocks2/core" "github.com/Dreamacro/go-shadowsocks2/core"
) )
@ -29,6 +28,7 @@ type ShadowSocks struct {
} }
type ShadowSocksOption struct { type ShadowSocksOption struct {
BasicOption
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port"` Port int `proxy:"port"`
@ -75,8 +75,8 @@ func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, e
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr) c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
@ -88,9 +88,9 @@ func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (_
return NewConn(c, ss), err return NewConn(c, ss), err
} }
// DialUDP implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := dialer.ListenPacket("udp", "") pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -105,13 +105,6 @@ func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ss), nil return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ss), nil
} }
// MarshalJSON implements C.ProxyAdapter
func (ss *ShadowSocks) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": ss.Type().String(),
})
}
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
cipher := option.Cipher cipher := option.Cipher
@ -157,16 +150,16 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
if opts.TLS { if opts.TLS {
v2rayOption.TLS = true v2rayOption.TLS = true
v2rayOption.SkipCertVerify = opts.SkipCertVerify v2rayOption.SkipCertVerify = opts.SkipCertVerify
v2rayOption.SessionCache = getClientSessionCache()
} }
} }
return &ShadowSocks{ return &ShadowSocks{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: addr, addr: addr,
tp: C.Shadowsocks, tp: C.Shadowsocks,
udp: option.UDP, udp: option.UDP,
iface: option.Interface,
}, },
cipher: ciph, cipher: ciph,

View File

@ -2,15 +2,14 @@ package outbound
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"net" "net"
"strconv" "strconv"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/ssr/obfs"
"github.com/Dreamacro/clash/component/ssr/protocol"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/ssr/obfs"
"github.com/Dreamacro/clash/transport/ssr/protocol"
"github.com/Dreamacro/go-shadowsocks2/core" "github.com/Dreamacro/go-shadowsocks2/core"
"github.com/Dreamacro/go-shadowsocks2/shadowaead" "github.com/Dreamacro/go-shadowsocks2/shadowaead"
@ -25,6 +24,7 @@ type ShadowSocksR struct {
} }
type ShadowSocksROption struct { type ShadowSocksROption struct {
BasicOption
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port"` Port int `proxy:"port"`
@ -60,8 +60,8 @@ func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn,
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ssr.addr) c, err := dialer.DialContext(ctx, "tcp", ssr.addr, ssr.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err) return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err)
} }
@ -73,9 +73,9 @@ func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata)
return NewConn(c, ssr), err return NewConn(c, ssr), err
} }
// DialUDP implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (ssr *ShadowSocksR) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := dialer.ListenPacket("udp", "") pc, err := dialer.ListenPacket(ctx, "udp", "", ssr.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -91,13 +91,6 @@ func (ssr *ShadowSocksR) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ssr), nil return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ssr), nil
} }
// MarshalJSON implements C.ProxyAdapter
func (ssr *ShadowSocksR) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": ssr.Type().String(),
})
}
func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) { func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
cipher := option.Cipher cipher := option.Cipher
@ -144,10 +137,11 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
return &ShadowSocksR{ return &ShadowSocksR{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: addr, addr: addr,
tp: C.ShadowsocksR, tp: C.ShadowsocksR,
udp: option.UDP, udp: option.UDP,
iface: option.Interface,
}, },
cipher: coreCiph, cipher: coreCiph,
obfs: obfs, obfs: obfs,

View File

@ -8,9 +8,9 @@ import (
"github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/common/structure"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
obfs "github.com/Dreamacro/clash/component/simple-obfs"
"github.com/Dreamacro/clash/component/snell"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
obfs "github.com/Dreamacro/clash/transport/simple-obfs"
"github.com/Dreamacro/clash/transport/snell"
) )
type Snell struct { type Snell struct {
@ -22,6 +22,7 @@ type Snell struct {
} }
type SnellOption struct { type SnellOption struct {
BasicOption
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port"` Port int `proxy:"port"`
@ -51,20 +52,20 @@ func streamConn(c net.Conn, option streamOption) *snell.Snell {
// StreamConn implements C.ProxyAdapter // StreamConn implements C.ProxyAdapter
func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption}) c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
port, _ := strconv.Atoi(metadata.DstPort) port, _ := strconv.ParseInt(metadata.DstPort, 10, 16)
err := snell.WriteHeader(c, metadata.String(), uint(port), s.version) err := snell.WriteHeader(c, metadata.String(), uint(port), s.version)
return c, err return c, err
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
if s.version == snell.Version2 { if s.version == snell.Version2 && len(opts) == 0 {
c, err := s.pool.Get() c, err := s.pool.Get()
if err != nil { if err != nil {
return nil, err return nil, err
} }
port, _ := strconv.Atoi(metadata.DstPort) port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
if err = snell.WriteHeader(c, metadata.String(), uint(port), s.version); err != nil { if err = snell.WriteHeader(c, metadata.String(), uint(port), s.version); err != nil {
c.Close() c.Close()
return nil, err return nil, err
@ -72,7 +73,7 @@ func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn
return NewConn(c, s), err return NewConn(c, s), err
} }
c, err := dialer.DialContext(ctx, "tcp", s.addr) c, err := dialer.DialContext(ctx, "tcp", s.addr, s.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", s.addr, err) return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
} }
@ -111,9 +112,10 @@ func NewSnell(option SnellOption) (*Snell, error) {
s := &Snell{ s := &Snell{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: addr, addr: addr,
tp: C.Snell, tp: C.Snell,
iface: option.Interface,
}, },
psk: psk, psk: psk,
obfsOption: obfsOption, obfsOption: obfsOption,
@ -122,7 +124,7 @@ func NewSnell(option SnellOption) (*Snell, error) {
if option.Version == snell.Version2 { if option.Version == snell.Version2 {
s.pool = snell.NewPool(func(ctx context.Context) (*snell.Snell, error) { s.pool = snell.NewPool(func(ctx context.Context) (*snell.Snell, error) {
c, err := dialer.DialContext(ctx, "tcp", addr) c, err := dialer.DialContext(ctx, "tcp", addr, s.Base.DialOptions()...)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -6,13 +6,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net" "net"
"strconv" "strconv"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/socks5"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5"
) )
type Socks5 struct { type Socks5 struct {
@ -25,6 +24,7 @@ type Socks5 struct {
} }
type Socks5Option struct { type Socks5Option struct {
BasicOption
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port"` Port int `proxy:"port"`
@ -60,8 +60,8 @@ func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr) c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
@ -77,11 +77,9 @@ func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Co
return NewConn(c, ss), nil return NewConn(c, ss), nil
} }
// DialUDP implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) { func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout) c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
defer cancel()
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
if err != nil { if err != nil {
err = fmt.Errorf("%s connect error: %w", ss.addr, err) err = fmt.Errorf("%s connect error: %w", ss.addr, err)
return return
@ -110,13 +108,13 @@ func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
return return
} }
pc, err := dialer.ListenPacket("udp", "") pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return return
} }
go func() { go func() {
io.Copy(ioutil.Discard, c) io.Copy(io.Discard, c)
c.Close() c.Close()
// A UDP association terminates when the TCP connection that the UDP // A UDP association terminates when the TCP connection that the UDP
// ASSOCIATE request arrived on terminates. RFC1928 // ASSOCIATE request arrived on terminates. RFC1928
@ -145,17 +143,17 @@ func NewSocks5(option Socks5Option) *Socks5 {
if option.TLS { if option.TLS {
tlsConfig = &tls.Config{ tlsConfig = &tls.Config{
InsecureSkipVerify: option.SkipCertVerify, InsecureSkipVerify: option.SkipCertVerify,
ClientSessionCache: getClientSessionCache(),
ServerName: option.Server, ServerName: option.Server,
} }
} }
return &Socks5{ return &Socks5{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Socks5, tp: C.Socks5,
udp: option.UDP, udp: option.UDP,
iface: option.Interface,
}, },
user: option.UserName, user: option.UserName,
pass: option.Password, pass: option.Password,

View File

@ -3,15 +3,15 @@ package outbound
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"encoding/json"
"fmt" "fmt"
"net" "net"
"net/http"
"strconv" "strconv"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/gun"
"github.com/Dreamacro/clash/component/trojan"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/trojan"
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )
@ -19,6 +19,7 @@ import (
type Trojan struct { type Trojan struct {
*Base *Base
instance *trojan.Trojan instance *trojan.Trojan
option *TrojanOption
// for gun mux // for gun mux
gunTLSConfig *tls.Config gunTLSConfig *tls.Config
@ -27,6 +28,7 @@ type Trojan struct {
} }
type TrojanOption struct { type TrojanOption struct {
BasicOption
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port"` Port int `proxy:"port"`
@ -37,6 +39,34 @@ type TrojanOption struct {
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"` Network string `proxy:"network,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
}
func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) {
if t.option.Network == "ws" {
host, port, _ := net.SplitHostPort(t.addr)
wsOpts := &trojan.WebsocketOption{
Host: host,
Port: port,
Path: t.option.WSOpts.Path,
}
if t.option.SNI != "" {
wsOpts.Host = t.option.SNI
}
if len(t.option.WSOpts.Headers) != 0 {
header := http.Header{}
for key, value := range t.option.WSOpts.Headers {
header.Add(key, value)
}
wsOpts.Headers = header
}
return t.instance.StreamWebsocketConn(c, wsOpts)
}
return t.instance.StreamConn(c)
} }
// StreamConn implements C.ProxyAdapter // StreamConn implements C.ProxyAdapter
@ -45,7 +75,7 @@ func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
if t.transport != nil { if t.transport != nil {
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig) c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig)
} else { } else {
c, err = t.instance.StreamConn(c) c, err = t.plainStream(c)
} }
if err != nil { if err != nil {
@ -57,9 +87,9 @@ func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
// gun transport // gun transport
if t.transport != nil { if t.transport != nil && len(opts) == 0 {
c, err := gun.StreamGunWithTransport(t.transport, t.gunConfig) c, err := gun.StreamGunWithTransport(t.transport, t.gunConfig)
if err != nil { if err != nil {
return nil, err return nil, err
@ -73,7 +103,7 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con
return NewConn(c, t), nil return NewConn(c, t), nil
} }
c, err := dialer.DialContext(ctx, "tcp", t.addr) c, err := dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
@ -89,27 +119,25 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con
return NewConn(c, t), err return NewConn(c, t), err
} }
// DialUDP implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (t *Trojan) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) { func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
var c net.Conn var c net.Conn
// grpc transport // grpc transport
if t.transport != nil { if t.transport != nil && len(opts) == 0 {
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig) c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
defer safeConnClose(c, err) defer safeConnClose(c, err)
} else { } else {
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout) c, err = dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
defer cancel()
c, err = dialer.DialContext(ctx, "tcp", t.addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
defer safeConnClose(c, err) defer safeConnClose(c, err)
tcpKeepAlive(c) tcpKeepAlive(c)
c, err = t.instance.StreamConn(c) c, err = t.plainStream(c)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
@ -124,21 +152,14 @@ func (t *Trojan) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
return newPacketConn(pc, t), err return newPacketConn(pc, t), err
} }
func (t *Trojan) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": t.Type().String(),
})
}
func NewTrojan(option TrojanOption) (*Trojan, error) { func NewTrojan(option TrojanOption) (*Trojan, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
tOption := &trojan.Option{ tOption := &trojan.Option{
Password: option.Password, Password: option.Password,
ALPN: option.ALPN, ALPN: option.ALPN,
ServerName: option.Server, ServerName: option.Server,
SkipCertVerify: option.SkipCertVerify, SkipCertVerify: option.SkipCertVerify,
ClientSessionCache: getClientSessionCache(),
} }
if option.SNI != "" { if option.SNI != "" {
@ -147,17 +168,19 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
t := &Trojan{ t := &Trojan{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: addr, addr: addr,
tp: C.Trojan, tp: C.Trojan,
udp: option.UDP, udp: option.UDP,
iface: option.Interface,
}, },
instance: trojan.New(tOption), instance: trojan.New(tOption),
option: &option,
} }
if option.Network == "grpc" { if option.Network == "grpc" {
dialFn := func(network, addr string) (net.Conn, error) { dialFn := func(network, addr string) (net.Conn, error) {
c, err := dialer.DialContext(context.Background(), "tcp", t.addr) c, err := dialer.DialContext(context.Background(), "tcp", t.addr, t.Base.DialOptions()...)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error())
} }
@ -170,7 +193,6 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
InsecureSkipVerify: tOption.SkipCertVerify, InsecureSkipVerify: tOption.SkipCertVerify,
ServerName: tOption.ServerName, ServerName: tOption.ServerName,
ClientSessionCache: getClientSessionCache(),
} }
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig) t.transport = gun.NewHTTP2Client(dialFn, tlsConfig)

View File

@ -2,56 +2,15 @@ package outbound
import ( import (
"bytes" "bytes"
"crypto/tls"
"fmt"
"net" "net"
"net/url"
"strconv" "strconv"
"sync"
"time" "time"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
"github.com/Dreamacro/clash/component/socks5"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5"
) )
const (
tcpTimeout = 5 * time.Second
)
var (
globalClientSessionCache tls.ClientSessionCache
once sync.Once
)
func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
u, err := url.Parse(rawURL)
if err != nil {
return
}
port := u.Port()
if port == "" {
switch u.Scheme {
case "https":
port = "443"
case "http":
port = "80"
default:
err = fmt.Errorf("%s scheme not Support", rawURL)
return
}
}
addr = C.Metadata{
AddrType: C.AtypDomainName,
Host: u.Hostname(),
DstIP: nil,
DstPort: port,
}
return
}
func tcpKeepAlive(c net.Conn) { func tcpKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok { if tcp, ok := c.(*net.TCPConn); ok {
tcp.SetKeepAlive(true) tcp.SetKeepAlive(true)
@ -59,17 +18,10 @@ func tcpKeepAlive(c net.Conn) {
} }
} }
func getClientSessionCache() tls.ClientSessionCache {
once.Do(func() {
globalClientSessionCache = tls.NewLRUClientSessionCache(128)
})
return globalClientSessionCache
}
func serializesSocksAddr(metadata *C.Metadata) []byte { func serializesSocksAddr(metadata *C.Metadata) []byte {
var buf [][]byte var buf [][]byte
aType := uint8(metadata.AddrType) aType := uint8(metadata.AddrType)
p, _ := strconv.Atoi(metadata.DstPort) p, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
port := []byte{uint8(p >> 8), uint8(p & 0xff)} port := []byte{uint8(p >> 8), uint8(p & 0xff)}
switch metadata.AddrType { switch metadata.AddrType {
case socks5.AtypDomainName: case socks5.AtypDomainName:

View File

@ -11,10 +11,10 @@ import (
"strings" "strings"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/gun"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
"github.com/Dreamacro/clash/component/vmess"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/vmess"
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )
@ -31,22 +31,26 @@ type Vmess struct {
} }
type VmessOption struct { type VmessOption struct {
Name string `proxy:"name"` BasicOption
Server string `proxy:"server"` Name string `proxy:"name"`
Port int `proxy:"port"` Server string `proxy:"server"`
UUID string `proxy:"uuid"` Port int `proxy:"port"`
AlterID int `proxy:"alterId"` UUID string `proxy:"uuid"`
Cipher string `proxy:"cipher"` AlterID int `proxy:"alterId"`
TLS bool `proxy:"tls,omitempty"` Cipher string `proxy:"cipher"`
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"` Network string `proxy:"network,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` TLS bool `proxy:"tls,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` ServerName string `proxy:"servername,omitempty"`
WSPath string `proxy:"ws-path,omitempty"` HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
WSHeaders map[string]string `proxy:"ws-headers,omitempty"` HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
ServerName string `proxy:"servername,omitempty"` WSOpts WSOptions `proxy:"ws-opts,omitempty"`
// TODO: remove these until 2022
WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
WSPath string `proxy:"ws-path,omitempty"`
} }
type HTTPOptions struct { type HTTPOptions struct {
@ -64,21 +68,37 @@ type GrpcOptions struct {
GrpcServiceName string `proxy:"grpc-service-name,omitempty"` GrpcServiceName string `proxy:"grpc-service-name,omitempty"`
} }
type WSOptions struct {
Path string `proxy:"path,omitempty"`
Headers map[string]string `proxy:"headers,omitempty"`
MaxEarlyData int `proxy:"max-early-data,omitempty"`
EarlyDataHeaderName string `proxy:"early-data-header-name,omitempty"`
}
// StreamConn implements C.ProxyAdapter // StreamConn implements C.ProxyAdapter
func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error var err error
switch v.option.Network { switch v.option.Network {
case "ws": case "ws":
host, port, _ := net.SplitHostPort(v.addr) if v.option.WSOpts.Path == "" {
wsOpts := &vmess.WebsocketConfig{ v.option.WSOpts.Path = v.option.WSPath
Host: host, }
Port: port, if len(v.option.WSOpts.Headers) == 0 {
Path: v.option.WSPath, v.option.WSOpts.Headers = v.option.WSHeaders
} }
if len(v.option.WSHeaders) != 0 { host, port, _ := net.SplitHostPort(v.addr)
wsOpts := &vmess.WebsocketConfig{
Host: host,
Port: port,
Path: v.option.WSOpts.Path,
MaxEarlyData: v.option.WSOpts.MaxEarlyData,
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
}
if len(v.option.WSOpts.Headers) != 0 {
header := http.Header{} header := http.Header{}
for key, value := range v.option.WSHeaders { for key, value := range v.option.WSOpts.Headers {
header.Add(key, value) header.Add(key, value)
} }
wsOpts.Headers = header wsOpts.Headers = header
@ -86,9 +106,16 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if v.option.TLS { if v.option.TLS {
wsOpts.TLS = true wsOpts.TLS = true
wsOpts.SessionCache = getClientSessionCache() wsOpts.TLSConfig = &tls.Config{
wsOpts.SkipCertVerify = v.option.SkipCertVerify ServerName: host,
wsOpts.ServerName = v.option.ServerName InsecureSkipVerify: v.option.SkipCertVerify,
NextProtos: []string{"http/1.1"},
}
if v.option.ServerName != "" {
wsOpts.TLSConfig.ServerName = v.option.ServerName
} else if host := wsOpts.Headers.Get("Host"); host != "" {
wsOpts.TLSConfig.ServerName = host
}
} }
c, err = vmess.StreamWebsocketConn(c, wsOpts) c, err = vmess.StreamWebsocketConn(c, wsOpts)
case "http": case "http":
@ -98,7 +125,6 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
tlsOpts := &vmess.TLSConfig{ tlsOpts := &vmess.TLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
SessionCache: getClientSessionCache(),
} }
if v.option.ServerName != "" { if v.option.ServerName != "" {
@ -125,7 +151,6 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
tlsOpts := vmess.TLSConfig{ tlsOpts := vmess.TLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
SessionCache: getClientSessionCache(),
NextProtos: []string{"h2"}, NextProtos: []string{"h2"},
} }
@ -153,7 +178,6 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
tlsOpts := &vmess.TLSConfig{ tlsOpts := &vmess.TLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
SessionCache: getClientSessionCache(),
} }
if v.option.ServerName != "" { if v.option.ServerName != "" {
@ -172,9 +196,9 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
// gun transport // gun transport
if v.transport != nil { if v.transport != nil && len(opts) == 0 {
c, err := gun.StreamGunWithTransport(v.transport, v.gunConfig) c, err := gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil { if err != nil {
return nil, err return nil, err
@ -189,7 +213,7 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn
return NewConn(c, v), nil return NewConn(c, v), nil
} }
c, err := dialer.DialContext(ctx, "tcp", v.addr) c, err := dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
@ -200,8 +224,8 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn
return NewConn(c, v), err return NewConn(c, v), err
} }
// DialUDP implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (v *Vmess) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) { func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
// vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr // vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr
if !metadata.Resolved() { if !metadata.Resolved() {
ip, err := resolver.ResolveIP(metadata.Host) ip, err := resolver.ResolveIP(metadata.Host)
@ -213,7 +237,7 @@ func (v *Vmess) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
var c net.Conn var c net.Conn
// gun transport // gun transport
if v.transport != nil { if v.transport != nil && len(opts) == 0 {
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig) c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil { if err != nil {
return nil, err return nil, err
@ -222,9 +246,7 @@ func (v *Vmess) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
c, err = v.client.StreamConn(c, parseVmessAddr(metadata)) c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
} else { } else {
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout) c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
defer cancel()
c, err = dialer.DialContext(ctx, "tcp", v.addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
@ -264,10 +286,11 @@ func NewVmess(option VmessOption) (*Vmess, error) {
v := &Vmess{ v := &Vmess{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Vmess, tp: C.Vmess,
udp: option.UDP, udp: option.UDP,
iface: option.Interface,
}, },
client: client, client: client,
option: &option, option: &option,
@ -280,7 +303,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
} }
case "grpc": case "grpc":
dialFn := func(network, addr string) (net.Conn, error) { dialFn := func(network, addr string) (net.Conn, error) {
c, err := dialer.DialContext(context.Background(), "tcp", v.addr) c, err := dialer.DialContext(context.Background(), "tcp", v.addr, v.Base.DialOptions()...)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
@ -330,7 +353,7 @@ func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
copy(addr[1:], []byte(metadata.Host)) copy(addr[1:], []byte(metadata.Host))
} }
port, _ := strconv.Atoi(metadata.DstPort) port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
return &vmess.DstAddr{ return &vmess.DstAddr{
UDP: metadata.NetWork == C.UDP, UDP: metadata.NetWork == C.UDP,
AddrType: addrType, AddrType: addrType,

View File

@ -3,8 +3,8 @@ package outboundgroup
import ( import (
"time" "time"
"github.com/Dreamacro/clash/adapters/provider"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
) )
const ( const (

View File

@ -4,10 +4,11 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/Dreamacro/clash/adapters/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/common/singledo" "github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
) )
type Fallback struct { type Fallback struct {
@ -23,19 +24,19 @@ func (f *Fallback) Now() string {
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
proxy := f.findAliveProxy(true) proxy := f.findAliveProxy(true)
c, err := proxy.DialContext(ctx, metadata) c, err := proxy.DialContext(ctx, metadata, f.Base.DialOptions(opts...)...)
if err == nil { if err == nil {
c.AppendToChains(f) c.AppendToChains(f)
} }
return c, err return c, err
} }
// DialUDP implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { func (f *Fallback) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
proxy := f.findAliveProxy(true) proxy := f.findAliveProxy(true)
pc, err := proxy.DialUDP(metadata) pc, err := proxy.ListenPacketContext(ctx, metadata, f.Base.DialOptions(opts...)...)
if err == nil { if err == nil {
pc.AppendToChains(f) pc.AppendToChains(f)
} }
@ -90,11 +91,16 @@ func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
return proxies[0] return proxies[0]
} }
func NewFallback(options *GroupCommonOption, providers []provider.ProxyProvider) *Fallback { func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
return &Fallback{ return &Fallback{
Base: outbound.NewBase(options.Name, "", C.Fallback, false), Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.Fallback,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle(defaultGetProxiesDuration), single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers, providers: providers,
disableUDP: options.DisableUDP, disableUDP: option.DisableUDP,
} }
} }

View File

@ -7,11 +7,12 @@ import (
"fmt" "fmt"
"net" "net"
"github.com/Dreamacro/clash/adapters/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/common/murmur3" "github.com/Dreamacro/clash/common/murmur3"
"github.com/Dreamacro/clash/common/singledo" "github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
"golang.org/x/net/publicsuffix" "golang.org/x/net/publicsuffix"
) )
@ -69,7 +70,7 @@ func jumpHash(key uint64, buckets int32) int32 {
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) { func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
defer func() { defer func() {
if err == nil { if err == nil {
c.AppendToChains(lb) c.AppendToChains(lb)
@ -78,12 +79,12 @@ func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata) (c
proxy := lb.Unwrap(metadata) proxy := lb.Unwrap(metadata)
c, err = proxy.DialContext(ctx, metadata) c, err = proxy.DialContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
return return
} }
// DialUDP implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (pc C.PacketConn, err error) { func (lb *LoadBalance) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (pc C.PacketConn, err error) {
defer func() { defer func() {
if err == nil { if err == nil {
pc.AppendToChains(lb) pc.AppendToChains(lb)
@ -91,8 +92,7 @@ func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (pc C.PacketConn, err error
}() }()
proxy := lb.Unwrap(metadata) proxy := lb.Unwrap(metadata)
return proxy.ListenPacketContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
return proxy.DialUDP(metadata)
} }
// SupportUDP implements C.ProxyAdapter // SupportUDP implements C.ProxyAdapter
@ -159,7 +159,7 @@ func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
}) })
} }
func NewLoadBalance(options *GroupCommonOption, providers []provider.ProxyProvider, strategy string) (lb *LoadBalance, err error) { func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvider, strategy string) (lb *LoadBalance, err error) {
var strategyFn strategyFn var strategyFn strategyFn
switch strategy { switch strategy {
case "consistent-hashing": case "consistent-hashing":
@ -170,10 +170,15 @@ func NewLoadBalance(options *GroupCommonOption, providers []provider.ProxyProvid
return nil, fmt.Errorf("%w: %s", errStrategy, strategy) return nil, fmt.Errorf("%w: %s", errStrategy, strategy)
} }
return &LoadBalance{ return &LoadBalance{
Base: outbound.NewBase(options.Name, "", C.LoadBalance, false), Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.LoadBalance,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle(defaultGetProxiesDuration), single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers, providers: providers,
strategyFn: strategyFn, strategyFn: strategyFn,
disableUDP: options.DisableUDP, disableUDP: option.DisableUDP,
}, nil }, nil
} }

View File

@ -4,9 +4,11 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/Dreamacro/clash/adapters/provider" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapter/provider"
"github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/common/structure"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider"
) )
var ( var (
@ -18,6 +20,7 @@ var (
) )
type GroupCommonOption struct { type GroupCommonOption struct {
outbound.BasicOption
Name string `group:"name"` Name string `group:"name"`
Type string `group:"type"` Type string `group:"type"`
Proxies []string `group:"proxies,omitempty"` Proxies []string `group:"proxies,omitempty"`
@ -28,7 +31,7 @@ type GroupCommonOption struct {
DisableUDP bool `group:"disable-udp,omitempty"` DisableUDP bool `group:"disable-udp,omitempty"`
} }
func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy, providersMap map[string]provider.ProxyProvider) (C.ProxyAdapter, error) { func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true}) decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true})
groupOption := &GroupCommonOption{ groupOption := &GroupCommonOption{
@ -44,7 +47,7 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
groupName := groupOption.Name groupName := groupOption.Name
providers := []provider.ProxyProvider{} providers := []types.ProxyProvider{}
if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 { if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 {
return nil, errMissProxy return nil, errMissProxy
@ -138,15 +141,15 @@ func getProxies(mapping map[string]C.Proxy, list []string) ([]C.Proxy, error) {
return ps, nil return ps, nil
} }
func getProviders(mapping map[string]provider.ProxyProvider, list []string) ([]provider.ProxyProvider, error) { func getProviders(mapping map[string]types.ProxyProvider, list []string) ([]types.ProxyProvider, error) {
var ps []provider.ProxyProvider var ps []types.ProxyProvider
for _, name := range list { for _, name := range list {
p, ok := mapping[name] p, ok := mapping[name]
if !ok { if !ok {
return nil, fmt.Errorf("'%s' not found", name) return nil, fmt.Errorf("'%s' not found", name)
} }
if p.VehicleType() == provider.Compatible { if p.VehicleType() == types.Compatible {
return nil, fmt.Errorf("proxy group %s can't contains in `use`", name) return nil, fmt.Errorf("proxy group %s can't contains in `use`", name)
} }
ps = append(ps, p) ps = append(ps, p)

View File

@ -3,14 +3,13 @@ package outboundgroup
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"github.com/Dreamacro/clash/adapters/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/common/singledo" "github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
) )
type Relay struct { type Relay struct {
@ -20,15 +19,25 @@ type Relay struct {
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
proxies := r.proxies(metadata, true) var proxies []C.Proxy
if len(proxies) == 0 { for _, proxy := range r.proxies(metadata, true) {
return nil, errors.New("proxy does not exist") if proxy.Type() != C.Direct {
proxies = append(proxies, proxy)
}
} }
switch len(proxies) {
case 0:
return outbound.NewDirect().DialContext(ctx, metadata, r.Base.DialOptions(opts...)...)
case 1:
return proxies[0].DialContext(ctx, metadata, r.Base.DialOptions(opts...)...)
}
first := proxies[0] first := proxies[0]
last := proxies[len(proxies)-1] last := proxies[len(proxies)-1]
c, err := dialer.DialContext(ctx, "tcp", first.Addr()) c, err := dialer.DialContext(ctx, "tcp", first.Addr(), r.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err) return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
} }
@ -91,9 +100,14 @@ func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy {
return proxies return proxies
} }
func NewRelay(options *GroupCommonOption, providers []provider.ProxyProvider) *Relay { func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
return &Relay{ return &Relay{
Base: outbound.NewBase(options.Name, "", C.Relay, false), Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.Relay,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle(defaultGetProxiesDuration), single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers, providers: providers,
} }

View File

@ -5,10 +5,11 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"github.com/Dreamacro/clash/adapters/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/common/singledo" "github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
) )
type Selector struct { type Selector struct {
@ -20,17 +21,17 @@ type Selector struct {
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
c, err := s.selectedProxy(true).DialContext(ctx, metadata) c, err := s.selectedProxy(true).DialContext(ctx, metadata, s.Base.DialOptions(opts...)...)
if err == nil { if err == nil {
c.AppendToChains(s) c.AppendToChains(s)
} }
return c, err return c, err
} }
// DialUDP implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (s *Selector) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { func (s *Selector) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := s.selectedProxy(true).DialUDP(metadata) pc, err := s.selectedProxy(true).ListenPacketContext(ctx, metadata, s.Base.DialOptions(opts...)...)
if err == nil { if err == nil {
pc.AppendToChains(s) pc.AppendToChains(s)
} }
@ -96,13 +97,18 @@ func (s *Selector) selectedProxy(touch bool) C.Proxy {
return elm.(C.Proxy) return elm.(C.Proxy)
} }
func NewSelector(options *GroupCommonOption, providers []provider.ProxyProvider) *Selector { func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider) *Selector {
selected := providers[0].Proxies()[0].Name() selected := providers[0].Proxies()[0].Name()
return &Selector{ return &Selector{
Base: outbound.NewBase(options.Name, "", C.Selector, false), Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.Selector,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle(defaultGetProxiesDuration), single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers, providers: providers,
selected: selected, selected: selected,
disableUDP: options.DisableUDP, disableUDP: option.DisableUDP,
} }
} }

View File

@ -5,10 +5,11 @@ import (
"encoding/json" "encoding/json"
"time" "time"
"github.com/Dreamacro/clash/adapters/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/common/singledo" "github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
) )
type urlTestOption func(*URLTest) type urlTestOption func(*URLTest)
@ -34,17 +35,17 @@ func (u *URLTest) Now() string {
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) { func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
c, err = u.fast(true).DialContext(ctx, metadata) c, err = u.fast(true).DialContext(ctx, metadata, u.Base.DialOptions(opts...)...)
if err == nil { if err == nil {
c.AppendToChains(u) c.AppendToChains(u)
} }
return c, err return c, err
} }
// DialUDP implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := u.fast(true).DialUDP(metadata) pc, err := u.fast(true).ListenPacketContext(ctx, metadata, u.Base.DialOptions(opts...)...)
if err == nil { if err == nil {
pc.AppendToChains(u) pc.AppendToChains(u)
} }
@ -133,13 +134,18 @@ func parseURLTestOption(config map[string]interface{}) []urlTestOption {
return opts return opts
} }
func NewURLTest(commonOptions *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest { func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
urlTest := &URLTest{ urlTest := &URLTest{
Base: outbound.NewBase(commonOptions.Name, "", C.URLTest, false), Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.URLTest,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle(defaultGetProxiesDuration), single: singledo.NewSingle(defaultGetProxiesDuration),
fastSingle: singledo.NewSingle(time.Second * 10), fastSingle: singledo.NewSingle(time.Second * 10),
providers: providers, providers: providers,
disableUDP: commonOptions.DisableUDP, disableUDP: option.DisableUDP,
} }
for _, option := range options { for _, option := range options {

View File

@ -1,8 +1,9 @@
package outbound package adapter
import ( import (
"fmt" "fmt"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/common/structure"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
) )
@ -20,36 +21,36 @@ func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) {
) )
switch proxyType { switch proxyType {
case "ss": case "ss":
ssOption := &ShadowSocksOption{} ssOption := &outbound.ShadowSocksOption{}
err = decoder.Decode(mapping, ssOption) err = decoder.Decode(mapping, ssOption)
if err != nil { if err != nil {
break break
} }
proxy, err = NewShadowSocks(*ssOption) proxy, err = outbound.NewShadowSocks(*ssOption)
case "ssr": case "ssr":
ssrOption := &ShadowSocksROption{} ssrOption := &outbound.ShadowSocksROption{}
err = decoder.Decode(mapping, ssrOption) err = decoder.Decode(mapping, ssrOption)
if err != nil { if err != nil {
break break
} }
proxy, err = NewShadowSocksR(*ssrOption) proxy, err = outbound.NewShadowSocksR(*ssrOption)
case "socks5": case "socks5":
socksOption := &Socks5Option{} socksOption := &outbound.Socks5Option{}
err = decoder.Decode(mapping, socksOption) err = decoder.Decode(mapping, socksOption)
if err != nil { if err != nil {
break break
} }
proxy = NewSocks5(*socksOption) proxy = outbound.NewSocks5(*socksOption)
case "http": case "http":
httpOption := &HttpOption{} httpOption := &outbound.HttpOption{}
err = decoder.Decode(mapping, httpOption) err = decoder.Decode(mapping, httpOption)
if err != nil { if err != nil {
break break
} }
proxy = NewHttp(*httpOption) proxy = outbound.NewHttp(*httpOption)
case "vmess": case "vmess":
vmessOption := &VmessOption{ vmessOption := &outbound.VmessOption{
HTTPOpts: HTTPOptions{ HTTPOpts: outbound.HTTPOptions{
Method: "GET", Method: "GET",
Path: []string{"/"}, Path: []string{"/"},
}, },
@ -58,21 +59,21 @@ func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) {
if err != nil { if err != nil {
break break
} }
proxy, err = NewVmess(*vmessOption) proxy, err = outbound.NewVmess(*vmessOption)
case "snell": case "snell":
snellOption := &SnellOption{} snellOption := &outbound.SnellOption{}
err = decoder.Decode(mapping, snellOption) err = decoder.Decode(mapping, snellOption)
if err != nil { if err != nil {
break break
} }
proxy, err = NewSnell(*snellOption) proxy, err = outbound.NewSnell(*snellOption)
case "trojan": case "trojan":
trojanOption := &TrojanOption{} trojanOption := &outbound.TrojanOption{}
err = decoder.Decode(mapping, trojanOption) err = decoder.Decode(mapping, trojanOption)
if err != nil { if err != nil {
break break
} }
proxy, err = NewTrojan(*trojanOption) proxy, err = outbound.NewTrojan(*trojanOption)
default: default:
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
} }

View File

@ -3,24 +3,24 @@ package provider
import ( import (
"bytes" "bytes"
"crypto/md5" "crypto/md5"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
types "github.com/Dreamacro/clash/constant/provider"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
) )
var ( var (
fileMode os.FileMode = 0666 fileMode os.FileMode = 0o666
dirMode os.FileMode = 0755 dirMode os.FileMode = 0o755
) )
type parser = func([]byte) (interface{}, error) type parser = func([]byte) (interface{}, error)
type fetcher struct { type fetcher struct {
name string name string
vehicle Vehicle vehicle types.Vehicle
updatedAt *time.Time updatedAt *time.Time
ticker *time.Ticker ticker *time.Ticker
done chan struct{} done chan struct{}
@ -33,7 +33,7 @@ func (f *fetcher) Name() string {
return f.name return f.name
} }
func (f *fetcher) VehicleType() VehicleType { func (f *fetcher) VehicleType() types.VehicleType {
return f.vehicle.Type() return f.vehicle.Type()
} }
@ -44,7 +44,7 @@ func (f *fetcher) Initial() (interface{}, error) {
isLocal bool isLocal bool
) )
if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil { if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil {
buf, err = ioutil.ReadFile(f.vehicle.Path()) buf, err = os.ReadFile(f.vehicle.Path())
modTime := stat.ModTime() modTime := stat.ModTime()
f.updatedAt = &modTime f.updatedAt = &modTime
isLocal = true isLocal = true
@ -76,7 +76,7 @@ func (f *fetcher) Initial() (interface{}, error) {
isLocal = false isLocal = false
} }
if f.vehicle.Type() != File && !isLocal { if f.vehicle.Type() != types.File && !isLocal {
if err := safeWrite(f.vehicle.Path(), buf); err != nil { if err := safeWrite(f.vehicle.Path(), buf); err != nil {
return nil, err return nil, err
} }
@ -110,7 +110,7 @@ func (f *fetcher) Update() (interface{}, bool, error) {
return nil, false, err return nil, false, err
} }
if f.vehicle.Type() != File { if f.vehicle.Type() != types.File {
if err := safeWrite(f.vehicle.Path(), buf); err != nil { if err := safeWrite(f.vehicle.Path(), buf); err != nil {
return nil, false, err return nil, false, err
} }
@ -164,10 +164,10 @@ func safeWrite(path string, buf []byte) error {
} }
} }
return ioutil.WriteFile(path, buf, fileMode) return os.WriteFile(path, buf, fileMode)
} }
func newFetcher(name string, interval time.Duration, vehicle Vehicle, parser parser, onUpdate func(interface{})) *fetcher { func newFetcher(name string, interval time.Duration, vehicle types.Vehicle, parser parser, onUpdate func(interface{})) *fetcher {
var ticker *time.Ticker var ticker *time.Ticker
if interval != 0 { if interval != 0 {
ticker = time.NewTicker(interval) ticker = time.NewTicker(interval)

View File

@ -2,9 +2,9 @@ package provider
import ( import (
"context" "context"
"sync"
"time" "time"
"github.com/Dreamacro/clash/common/batch"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"go.uber.org/atomic" "go.uber.org/atomic"
@ -59,20 +59,17 @@ func (hc *HealthCheck) touch() {
} }
func (hc *HealthCheck) check() { func (hc *HealthCheck) check() {
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout) b, _ := batch.New(context.Background(), batch.WithConcurrencyNum(10))
wg := &sync.WaitGroup{}
for _, proxy := range hc.proxies { for _, proxy := range hc.proxies {
wg.Add(1) p := proxy
b.Go(p.Name(), func() (interface{}, error) {
go func(p C.Proxy) { ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
defer cancel()
p.URLTest(ctx, hc.url) p.URLTest(ctx, hc.url)
wg.Done() return nil, nil
}(proxy) })
} }
b.Wait()
wg.Wait()
cancel()
} }
func (hc *HealthCheck) close() { func (hc *HealthCheck) close() {

View File

@ -7,11 +7,10 @@ import (
"github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/common/structure"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider"
) )
var ( var errVehicleType = errors.New("unsupport vehicle type")
errVehicleType = errors.New("unsupport vehicle type")
)
type healthCheckSchema struct { type healthCheckSchema struct {
Enable bool `provider:"enable"` Enable bool `provider:"enable"`
@ -28,7 +27,7 @@ type proxyProviderSchema struct {
HealthCheck healthCheckSchema `provider:"health-check,omitempty"` HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
} }
func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvider, error) { func ParseProxyProvider(name string, mapping map[string]interface{}) (types.ProxyProvider, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true}) decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
schema := &proxyProviderSchema{ schema := &proxyProviderSchema{
@ -48,7 +47,7 @@ func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvi
path := C.Path.Resolve(schema.Path) path := C.Path.Resolve(schema.Path)
var vehicle Vehicle var vehicle types.Vehicle
switch schema.Type { switch schema.Type {
case "file": case "file":
vehicle = NewFileVehicle(path) vehicle = NewFileVehicle(path)

View File

@ -7,8 +7,9 @@ import (
"runtime" "runtime"
"time" "time"
"github.com/Dreamacro/clash/adapters/outbound" "github.com/Dreamacro/clash/adapter"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
@ -17,45 +18,6 @@ const (
ReservedName = "default" ReservedName = "default"
) )
// Provider Type
const (
Proxy ProviderType = iota
Rule
)
// ProviderType defined
type ProviderType int
func (pt ProviderType) String() string {
switch pt {
case Proxy:
return "Proxy"
case Rule:
return "Rule"
default:
return "Unknown"
}
}
// Provider interface
type Provider interface {
Name() string
VehicleType() VehicleType
Type() ProviderType
Initial() error
Update() error
}
// ProxyProvider interface
type ProxyProvider interface {
Provider
Proxies() []C.Proxy
// ProxiesWithTouch is used to inform the provider that the proxy is actually being used while getting the list of proxies.
// Commonly used in Dial and DialUDP
ProxiesWithTouch() []C.Proxy
HealthCheck()
}
type ProxySchema struct { type ProxySchema struct {
Proxies []map[string]interface{} `yaml:"proxies"` Proxies []map[string]interface{} `yaml:"proxies"`
} }
@ -107,8 +69,8 @@ func (pp *proxySetProvider) Initial() error {
return nil return nil
} }
func (pp *proxySetProvider) Type() ProviderType { func (pp *proxySetProvider) Type() types.ProviderType {
return Proxy return types.Proxy
} }
func (pp *proxySetProvider) Proxies() []C.Proxy { func (pp *proxySetProvider) Proxies() []C.Proxy {
@ -133,7 +95,7 @@ func proxiesParse(buf []byte) (interface{}, error) {
proxies := []C.Proxy{} proxies := []C.Proxy{}
for idx, mapping := range schema.Proxies { for idx, mapping := range schema.Proxies {
proxy, err := outbound.ParseProxy(mapping) proxy, err := adapter.ParseProxy(mapping)
if err != nil { if err != nil {
return nil, fmt.Errorf("proxy %d error: %w", idx, err) return nil, fmt.Errorf("proxy %d error: %w", idx, err)
} }
@ -160,7 +122,7 @@ func stopProxyProvider(pd *ProxySetProvider) {
pd.fetcher.Destroy() pd.fetcher.Destroy()
} }
func NewProxySetProvider(name string, interval time.Duration, vehicle Vehicle, hc *HealthCheck) *ProxySetProvider { func NewProxySetProvider(name string, interval time.Duration, vehicle types.Vehicle, hc *HealthCheck) *ProxySetProvider {
if hc.auto() { if hc.auto() {
go hc.process() go hc.process()
} }
@ -219,12 +181,12 @@ func (cp *compatibleProvider) Initial() error {
return nil return nil
} }
func (cp *compatibleProvider) VehicleType() VehicleType { func (cp *compatibleProvider) VehicleType() types.VehicleType {
return Compatible return types.Compatible
} }
func (cp *compatibleProvider) Type() ProviderType { func (cp *compatibleProvider) Type() types.ProviderType {
return Proxy return types.Proxy
} }
func (cp *compatibleProvider) Proxies() []C.Proxy { func (cp *compatibleProvider) Proxies() []C.Proxy {
@ -242,7 +204,7 @@ func stopCompatibleProvider(pd *CompatibleProvider) {
func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvider, error) { func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvider, error) {
if len(proxies) == 0 { if len(proxies) == 0 {
return nil, errors.New("Provider need one proxy at least") return nil, errors.New("provider need one proxy at least")
} }
if hc.auto() { if hc.auto() {

View File

@ -2,49 +2,23 @@ package provider
import ( import (
"context" "context"
"io/ioutil" "io"
"net"
"net/http" "net/http"
"net/url" "net/url"
"os"
"time" "time"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
types "github.com/Dreamacro/clash/constant/provider"
) )
// Vehicle Type
const (
File VehicleType = iota
HTTP
Compatible
)
// VehicleType defined
type VehicleType int
func (v VehicleType) String() string {
switch v {
case File:
return "File"
case HTTP:
return "HTTP"
case Compatible:
return "Compatible"
default:
return "Unknown"
}
}
type Vehicle interface {
Read() ([]byte, error)
Path() string
Type() VehicleType
}
type FileVehicle struct { type FileVehicle struct {
path string path string
} }
func (f *FileVehicle) Type() VehicleType { func (f *FileVehicle) Type() types.VehicleType {
return File return types.File
} }
func (f *FileVehicle) Path() string { func (f *FileVehicle) Path() string {
@ -52,7 +26,7 @@ func (f *FileVehicle) Path() string {
} }
func (f *FileVehicle) Read() ([]byte, error) { func (f *FileVehicle) Read() ([]byte, error) {
return ioutil.ReadFile(f.path) return os.ReadFile(f.path)
} }
func NewFileVehicle(path string) *FileVehicle { func NewFileVehicle(path string) *FileVehicle {
@ -64,8 +38,8 @@ type HTTPVehicle struct {
path string path string
} }
func (h *HTTPVehicle) Type() VehicleType { func (h *HTTPVehicle) Type() types.VehicleType {
return HTTP return types.HTTP
} }
func (h *HTTPVehicle) Path() string { func (h *HTTPVehicle) Path() string {
@ -99,7 +73,9 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
IdleConnTimeout: 90 * time.Second, IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second, TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second, ExpectContinueTimeout: 1 * time.Second,
DialContext: dialer.DialContext, DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
return dialer.DialContext(ctx, network, address)
},
} }
client := http.Client{Transport: transport} client := http.Client{Transport: transport}
@ -109,7 +85,7 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
} }
defer resp.Body.Close() defer resp.Body.Close()
buf, err := ioutil.ReadAll(resp.Body) buf, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -1,61 +0,0 @@
package inbound
import (
"net"
"net/http"
"strings"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
)
// NewHTTP receive normal http request and return HTTPContext
func NewHTTP(request *http.Request, conn net.Conn) *context.HTTPContext {
metadata := parseHTTPAddr(request)
metadata.Type = C.HTTP
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
}
return context.NewHTTPContext(conn, request, metadata)
}
// RemoveHopByHopHeaders remove hop-by-hop header
func RemoveHopByHopHeaders(header http.Header) {
// Strip hop-by-hop header based on RFC:
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
// https://www.mnot.net/blog/2011/07/11/what_proxies_must_do
header.Del("Proxy-Connection")
header.Del("Proxy-Authenticate")
header.Del("Proxy-Authorization")
header.Del("TE")
header.Del("Trailers")
header.Del("Transfer-Encoding")
header.Del("Upgrade")
connections := header.Get("Connection")
header.Del("Connection")
if len(connections) == 0 {
return
}
for _, h := range strings.Split(connections, ",") {
header.Del(strings.TrimSpace(h))
}
}
// RemoveExtraHTTPHostPort remove extra host port (example.com:80 --> example.com)
// It resolves the behavior of some HTTP servers that do not handle host:80 (e.g. baidu.com)
func RemoveExtraHTTPHostPort(req *http.Request) {
host := req.Host
if host == "" {
host = req.URL.Host
}
if pHost, port, err := net.SplitHostPort(host); err == nil && port == "80" {
host = pHost
}
req.Host = host
req.URL.Host = host
}

105
common/batch/batch.go Normal file
View File

@ -0,0 +1,105 @@
package batch
import (
"context"
"sync"
)
type Option = func(b *Batch)
type Result struct {
Value interface{}
Err error
}
type Error struct {
Key string
Err error
}
func WithConcurrencyNum(n int) Option {
return func(b *Batch) {
q := make(chan struct{}, n)
for i := 0; i < n; i++ {
q <- struct{}{}
}
b.queue = q
}
}
// Batch similar to errgroup, but can control the maximum number of concurrent
type Batch struct {
result map[string]Result
queue chan struct{}
wg sync.WaitGroup
mux sync.Mutex
err *Error
once sync.Once
cancel func()
}
func (b *Batch) Go(key string, fn func() (interface{}, error)) {
b.wg.Add(1)
go func() {
defer b.wg.Done()
if b.queue != nil {
<-b.queue
defer func() {
b.queue <- struct{}{}
}()
}
value, err := fn()
if err != nil {
b.once.Do(func() {
b.err = &Error{key, err}
if b.cancel != nil {
b.cancel()
}
})
}
ret := Result{value, err}
b.mux.Lock()
defer b.mux.Unlock()
b.result[key] = ret
}()
}
func (b *Batch) Wait() *Error {
b.wg.Wait()
if b.cancel != nil {
b.cancel()
}
return b.err
}
func (b *Batch) WaitAndGetResult() (map[string]Result, *Error) {
err := b.Wait()
return b.Result(), err
}
func (b *Batch) Result() map[string]Result {
b.mux.Lock()
defer b.mux.Unlock()
copy := map[string]Result{}
for k, v := range b.result {
copy[k] = v
}
return copy
}
func New(ctx context.Context, opts ...Option) (*Batch, context.Context) {
ctx, cancel := context.WithCancel(ctx)
b := &Batch{
result: map[string]Result{},
}
for _, o := range opts {
o(b)
}
b.cancel = cancel
return b, ctx
}

View File

@ -0,0 +1,83 @@
package batch
import (
"context"
"errors"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestBatch(t *testing.T) {
b, _ := New(context.Background())
now := time.Now()
b.Go("foo", func() (interface{}, error) {
time.Sleep(time.Millisecond * 100)
return "foo", nil
})
b.Go("bar", func() (interface{}, error) {
time.Sleep(time.Millisecond * 150)
return "bar", nil
})
result, err := b.WaitAndGetResult()
assert.Nil(t, err)
duration := time.Since(now)
assert.Less(t, duration, time.Millisecond*200)
assert.Equal(t, 2, len(result))
for k, v := range result {
assert.NoError(t, v.Err)
assert.Equal(t, k, v.Value.(string))
}
}
func TestBatchWithConcurrencyNum(t *testing.T) {
b, _ := New(
context.Background(),
WithConcurrencyNum(3),
)
now := time.Now()
for i := 0; i < 7; i++ {
idx := i
b.Go(strconv.Itoa(idx), func() (interface{}, error) {
time.Sleep(time.Millisecond * 100)
return strconv.Itoa(idx), nil
})
}
result, _ := b.WaitAndGetResult()
duration := time.Since(now)
assert.Greater(t, duration, time.Millisecond*260)
assert.Equal(t, 7, len(result))
for k, v := range result {
assert.NoError(t, v.Err)
assert.Equal(t, k, v.Value.(string))
}
}
func TestBatchContext(t *testing.T) {
b, ctx := New(context.Background())
b.Go("error", func() (interface{}, error) {
time.Sleep(time.Millisecond * 100)
return nil, errors.New("test error")
})
b.Go("ctx", func() (interface{}, error) {
<-ctx.Done()
return nil, ctx.Err()
})
result, err := b.WaitAndGetResult()
assert.NotNil(t, err)
assert.Equal(t, "error", err.Key)
assert.Equal(t, ctx.Err(), result["ctx"].Err)
}

View File

@ -149,7 +149,6 @@ func TestSetWithExpire(t *testing.T) {
assert.Equal(t, nil, res) assert.Equal(t, nil, res)
assert.Equal(t, time.Time{}, expires) assert.Equal(t, time.Time{}, expires)
assert.Equal(t, false, exist) assert.Equal(t, false, exist)
} }
func TestStale(t *testing.T) { func TestStale(t *testing.T) {

View File

@ -67,7 +67,6 @@ func (d *digest32) bmix(p []byte) (tail []byte) {
} }
func (d *digest32) Sum32() (h1 uint32) { func (d *digest32) Sum32() (h1 uint32) {
h1 = d.h1 h1 = d.h1
var k1 uint32 var k1 uint32

View File

@ -1,4 +1,4 @@
package mixed package net
import ( import (
"bufio" "bufio"
@ -11,6 +11,9 @@ type BufferedConn struct {
} }
func NewBufferedConn(c net.Conn) *BufferedConn { func NewBufferedConn(c net.Conn) *BufferedConn {
if bc, ok := c.(*BufferedConn); ok {
return bc
}
return &BufferedConn{bufio.NewReader(c), c} return &BufferedConn{bufio.NewReader(c), c}
} }

View File

@ -38,7 +38,7 @@ func TestObservable_MultiSubscribe(t *testing.T) {
src := NewObservable(iter) src := NewObservable(iter)
ch1, _ := src.Subscribe() ch1, _ := src.Subscribe()
ch2, _ := src.Subscribe() ch2, _ := src.Subscribe()
var count = atomic.NewInt32(0) count := atomic.NewInt32(0)
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(2) wg.Add(2)

View File

@ -8,11 +8,7 @@ import (
"sync" "sync"
) )
var defaultAllocator *Allocator var defaultAllocator = NewAllocator()
func init() {
defaultAllocator = NewAllocator()
}
// Allocator for incoming frames, optimized to prevent overwriting after zeroing // Allocator for incoming frames, optimized to prevent overwriting after zeroing
type Allocator struct { type Allocator struct {
@ -57,6 +53,7 @@ func (alloc *Allocator) Put(buf []byte) error {
} }
//lint:ignore SA6002 ignore temporarily //lint:ignore SA6002 ignore temporarily
//nolint
alloc.buffers[bits].Put(buf) alloc.buffers[bits].Put(buf)
return nil return nil
} }

17
common/pool/buffer.go Normal file
View File

@ -0,0 +1,17 @@
package pool
import (
"bytes"
"sync"
)
var bufferPool = sync.Pool{New: func() interface{} { return &bytes.Buffer{} }}
func GetBuffer() *bytes.Buffer {
return bufferPool.Get().(*bytes.Buffer)
}
func PutBuffer(buf *bytes.Buffer) {
buf.Reset()
bufferPool.Put(buf)
}

View File

@ -5,6 +5,11 @@ const (
// but the maximum packet size of vmess/shadowsocks is about 16 KiB // but the maximum packet size of vmess/shadowsocks is about 16 KiB
// so define a buffer of 20 KiB to reduce the memory of each TCP relay // so define a buffer of 20 KiB to reduce the memory of each TCP relay
RelayBufferSize = 20 * 1024 RelayBufferSize = 20 * 1024
// RelayBufferSize uses 20KiB, but due to the allocator it will actually
// request 32Kib. Most UDPs are smaller than the MTU, and the TUN's MTU
// set to 9000, so the UDP Buffer size set to 16Kib
UDPBufferSize = 16 * 1024
) )
func Get(size int) []byte { func Get(size int) []byte {

View File

@ -12,7 +12,7 @@ import (
func TestBasic(t *testing.T) { func TestBasic(t *testing.T) {
single := NewSingle(time.Millisecond * 30) single := NewSingle(time.Millisecond * 30)
foo := 0 foo := 0
var shardCount = atomic.NewInt32(0) shardCount := atomic.NewInt32(0)
call := func() (interface{}, error) { call := func() (interface{}, error) {
foo++ foo++
time.Sleep(time.Millisecond * 5) time.Sleep(time.Millisecond * 5)

View File

@ -1,3 +1,4 @@
//go:build !linux
// +build !linux // +build !linux
package sockopt package sockopt

View File

@ -37,6 +37,12 @@ func (d *Decoder) Decode(src map[string]interface{}, dst interface{}) error {
v := reflect.ValueOf(dst).Elem() v := reflect.ValueOf(dst).Elem()
for idx := 0; idx < v.NumField(); idx++ { for idx := 0; idx < v.NumField(); idx++ {
field := t.Field(idx) field := t.Field(idx)
if field.Anonymous {
if err := d.decodeStruct(field.Name, src, v.Field(idx)); err != nil {
return err
}
continue
}
tag := field.Tag.Get(d.option.TagName) tag := field.Tag.Get(d.option.TagName)
str := strings.SplitN(tag, ",", 2) str := strings.SplitN(tag, ",", 2)

View File

@ -1,12 +1,15 @@
package structure package structure
import ( import (
"reflect"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
var decoder = NewDecoder(Option{TagName: "test"}) var (
var weakTypeDecoder = NewDecoder(Option{TagName: "test", WeaklyTypedInput: true}) decoder = NewDecoder(Option{TagName: "test"})
weakTypeDecoder = NewDecoder(Option{TagName: "test", WeaklyTypedInput: true})
)
type Baz struct { type Baz struct {
Foo int `test:"foo"` Foo int `test:"foo"`
@ -37,12 +40,8 @@ func TestStructure_Basic(t *testing.T) {
s := &Baz{} s := &Baz{}
err := decoder.Decode(rawMap, s) err := decoder.Decode(rawMap, s)
if err != nil { assert.Nil(t, err)
t.Fatal(err.Error()) assert.Equal(t, goal, s)
}
if !reflect.DeepEqual(s, goal) {
t.Fatalf("bad: %#v", s)
}
} }
func TestStructure_Slice(t *testing.T) { func TestStructure_Slice(t *testing.T) {
@ -58,12 +57,8 @@ func TestStructure_Slice(t *testing.T) {
s := &BazSlice{} s := &BazSlice{}
err := decoder.Decode(rawMap, s) err := decoder.Decode(rawMap, s)
if err != nil { assert.Nil(t, err)
t.Fatal(err.Error()) assert.Equal(t, goal, s)
}
if !reflect.DeepEqual(s, goal) {
t.Fatalf("bad: %#v", s)
}
} }
func TestStructure_Optional(t *testing.T) { func TestStructure_Optional(t *testing.T) {
@ -77,12 +72,8 @@ func TestStructure_Optional(t *testing.T) {
s := &BazOptional{} s := &BazOptional{}
err := decoder.Decode(rawMap, s) err := decoder.Decode(rawMap, s)
if err != nil { assert.Nil(t, err)
t.Fatal(err.Error()) assert.Equal(t, goal, s)
}
if !reflect.DeepEqual(s, goal) {
t.Fatalf("bad: %#v", s)
}
} }
func TestStructure_MissingKey(t *testing.T) { func TestStructure_MissingKey(t *testing.T) {
@ -92,18 +83,14 @@ func TestStructure_MissingKey(t *testing.T) {
s := &Baz{} s := &Baz{}
err := decoder.Decode(rawMap, s) err := decoder.Decode(rawMap, s)
if err == nil { assert.NotNilf(t, err, "should throw error: %#v", s)
t.Fatalf("should throw error: %#v", s)
}
} }
func TestStructure_ParamError(t *testing.T) { func TestStructure_ParamError(t *testing.T) {
rawMap := map[string]interface{}{} rawMap := map[string]interface{}{}
s := Baz{} s := Baz{}
err := decoder.Decode(rawMap, s) err := decoder.Decode(rawMap, s)
if err == nil { assert.NotNilf(t, err, "should throw error: %#v", s)
t.Fatalf("should throw error: %#v", s)
}
} }
func TestStructure_SliceTypeError(t *testing.T) { func TestStructure_SliceTypeError(t *testing.T) {
@ -114,9 +101,7 @@ func TestStructure_SliceTypeError(t *testing.T) {
s := &BazSlice{} s := &BazSlice{}
err := decoder.Decode(rawMap, s) err := decoder.Decode(rawMap, s)
if err == nil { assert.NotNilf(t, err, "should throw error: %#v", s)
t.Fatalf("should throw error: %#v", s)
}
} }
func TestStructure_WeakType(t *testing.T) { func TestStructure_WeakType(t *testing.T) {
@ -132,10 +117,23 @@ func TestStructure_WeakType(t *testing.T) {
s := &BazSlice{} s := &BazSlice{}
err := weakTypeDecoder.Decode(rawMap, s) err := weakTypeDecoder.Decode(rawMap, s)
if err != nil { assert.Nil(t, err)
t.Fatal(err.Error()) assert.Equal(t, goal, s)
} }
if !reflect.DeepEqual(s, goal) {
t.Fatalf("bad: %#v", s) func TestStructure_Nest(t *testing.T) {
} rawMap := map[string]interface{}{
"foo": 1,
}
goal := BazOptional{
Foo: 1,
}
s := &struct {
BazOptional
}{}
err := decoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Equal(t, s.BazOptional, goal)
} }

18
component/dhcp/conn.go Normal file
View File

@ -0,0 +1,18 @@
package dhcp
import (
"context"
"net"
"runtime"
"github.com/Dreamacro/clash/component/dialer"
)
func ListenDHCPClient(ctx context.Context, ifaceName string) (net.PacketConn, error) {
listenAddr := "0.0.0.0:68"
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
listenAddr = "255.255.255.255:68"
}
return dialer.ListenPacket(ctx, "udp4", listenAddr, dialer.WithInterface(ifaceName), dialer.WithAddrReuse(true))
}

88
component/dhcp/dhcp.go Normal file
View File

@ -0,0 +1,88 @@
package dhcp
import (
"context"
"errors"
"net"
"github.com/Dreamacro/clash/component/iface"
"github.com/insomniacslk/dhcp/dhcpv4"
)
var (
ErrNotResponding = errors.New("DHCP not responding")
ErrNotFound = errors.New("DNS option not found")
)
func ResolveDNSFromDHCP(context context.Context, ifaceName string) ([]net.IP, error) {
conn, err := ListenDHCPClient(context, ifaceName)
if err != nil {
return nil, err
}
defer conn.Close()
result := make(chan []net.IP, 1)
ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil {
return nil, err
}
discovery, err := dhcpv4.NewDiscovery(ifaceObj.HardwareAddr, dhcpv4.WithBroadcast(true), dhcpv4.WithRequestedOptions(dhcpv4.OptionDomainNameServer))
if err != nil {
return nil, err
}
go receiveOffer(conn, discovery.TransactionID, result)
_, err = conn.WriteTo(discovery.ToBytes(), &net.UDPAddr{IP: net.IPv4bcast, Port: 67})
if err != nil {
return nil, err
}
select {
case r, ok := <-result:
if !ok {
return nil, ErrNotFound
}
return r, nil
case <-context.Done():
return nil, ErrNotResponding
}
}
func receiveOffer(conn net.PacketConn, id dhcpv4.TransactionID, result chan<- []net.IP) {
defer close(result)
buf := make([]byte, dhcpv4.MaxMessageSize)
for {
n, _, err := conn.ReadFrom(buf)
if err != nil {
return
}
pkt, err := dhcpv4.FromBytes(buf[:n])
if err != nil {
continue
}
if pkt.MessageType() != dhcpv4.MessageTypeOffer {
continue
}
if pkt.TransactionID != id {
continue
}
dns := pkt.DNS()
if len(dns) == 0 {
return
}
result <- dns
return
}
}

View File

@ -1,118 +0,0 @@
package dialer
import (
"errors"
"net"
"time"
"github.com/Dreamacro/clash/common/singledo"
)
// In some OS, such as Windows, it takes a little longer to get interface information
var ifaceSingle = singledo.NewSingle(time.Second * 20)
var (
errPlatformNotSupport = errors.New("unsupport platform")
)
func lookupTCPAddr(ip net.IP, addrs []net.Addr) (*net.TCPAddr, error) {
ipv4 := ip.To4() != nil
for _, elm := range addrs {
addr, ok := elm.(*net.IPNet)
if !ok {
continue
}
addrV4 := addr.IP.To4() != nil
if addrV4 && ipv4 {
return &net.TCPAddr{IP: addr.IP, Port: 0}, nil
} else if !addrV4 && !ipv4 {
return &net.TCPAddr{IP: addr.IP, Port: 0}, nil
}
}
return nil, ErrAddrNotFound
}
func lookupUDPAddr(ip net.IP, addrs []net.Addr) (*net.UDPAddr, error) {
ipv4 := ip.To4() != nil
for _, elm := range addrs {
addr, ok := elm.(*net.IPNet)
if !ok {
continue
}
addrV4 := addr.IP.To4() != nil
if addrV4 && ipv4 {
return &net.UDPAddr{IP: addr.IP, Port: 0}, nil
} else if !addrV4 && !ipv4 {
return &net.UDPAddr{IP: addr.IP, Port: 0}, nil
}
}
return nil, ErrAddrNotFound
}
func fallbackBindToDialer(dialer *net.Dialer, network string, ip net.IP, name string) error {
if !ip.IsGlobalUnicast() {
return nil
}
iface, err, _ := ifaceSingle.Do(func() (interface{}, error) {
return net.InterfaceByName(name)
})
if err != nil {
return err
}
addrs, err := iface.(*net.Interface).Addrs()
if err != nil {
return err
}
switch network {
case "tcp", "tcp4", "tcp6":
if addr, err := lookupTCPAddr(ip, addrs); err == nil {
dialer.LocalAddr = addr
} else {
return err
}
case "udp", "udp4", "udp6":
if addr, err := lookupUDPAddr(ip, addrs); err == nil {
dialer.LocalAddr = addr
} else {
return err
}
}
return nil
}
func fallbackBindToListenConfig(name string) (string, error) {
iface, err, _ := ifaceSingle.Do(func() (interface{}, error) {
return net.InterfaceByName(name)
})
if err != nil {
return "", err
}
addrs, err := iface.(*net.Interface).Addrs()
if err != nil {
return "", err
}
for _, elm := range addrs {
addr, ok := elm.(*net.IPNet)
if !ok || addr.IP.To4() == nil {
continue
}
return net.JoinHostPort(addr.IP.String(), "0"), nil
}
return "", ErrAddrNotFound
}

View File

@ -3,51 +3,57 @@ package dialer
import ( import (
"net" "net"
"syscall" "syscall"
"golang.org/x/sys/unix"
"github.com/Dreamacro/clash/component/iface"
) )
type controlFn = func(network, address string, c syscall.RawConn) error type controlFn = func(network, address string, c syscall.RawConn) error
func bindControl(ifaceIdx int) controlFn { func bindControl(ifaceIdx int, chain controlFn) controlFn {
return func(network, address string, c syscall.RawConn) error { return func(network, address string, c syscall.RawConn) (err error) {
defer func() {
if err == nil && chain != nil {
err = chain(network, address, c)
}
}()
ipStr, _, err := net.SplitHostPort(address) ipStr, _, err := net.SplitHostPort(address)
if err == nil { if err == nil {
ip := net.ParseIP(ipStr) ip := net.ParseIP(ipStr)
if ip != nil && !ip.IsGlobalUnicast() { if ip != nil && !ip.IsGlobalUnicast() {
return nil return
} }
} }
return c.Control(func(fd uintptr) { return c.Control(func(fd uintptr) {
switch network { switch network {
case "tcp4", "udp4": case "tcp4", "udp4":
syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_BOUND_IF, ifaceIdx) unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, ifaceIdx)
case "tcp6", "udp6": case "tcp6", "udp6":
syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_BOUND_IF, ifaceIdx) unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, ifaceIdx)
} }
}) })
} }
} }
func bindIfaceToDialer(dialer *net.Dialer, ifaceName string) error { func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ net.IP) error {
iface, err, _ := ifaceSingle.Do(func() (interface{}, error) { ifaceObj, err := iface.ResolveInterface(ifaceName)
return net.InterfaceByName(ifaceName)
})
if err != nil { if err != nil {
return err return err
} }
dialer.Control = bindControl(iface.(*net.Interface).Index) dialer.Control = bindControl(ifaceObj.Index, dialer.Control)
return nil return nil
} }
func bindIfaceToListenConfig(lc *net.ListenConfig, ifaceName string) error { func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string) (string, error) {
iface, err, _ := ifaceSingle.Do(func() (interface{}, error) { ifaceObj, err := iface.ResolveInterface(ifaceName)
return net.InterfaceByName(ifaceName)
})
if err != nil { if err != nil {
return err return "", err
} }
lc.Control = bindControl(iface.(*net.Interface).Index) lc.Control = bindControl(ifaceObj.Index, lc.Control)
return nil return address, nil
} }

View File

@ -3,34 +3,42 @@ package dialer
import ( import (
"net" "net"
"syscall" "syscall"
"golang.org/x/sys/unix"
) )
type controlFn = func(network, address string, c syscall.RawConn) error type controlFn = func(network, address string, c syscall.RawConn) error
func bindControl(ifaceName string) controlFn { func bindControl(ifaceName string, chain controlFn) controlFn {
return func(network, address string, c syscall.RawConn) error { return func(network, address string, c syscall.RawConn) (err error) {
defer func() {
if err == nil && chain != nil {
err = chain(network, address, c)
}
}()
ipStr, _, err := net.SplitHostPort(address) ipStr, _, err := net.SplitHostPort(address)
if err == nil { if err == nil {
ip := net.ParseIP(ipStr) ip := net.ParseIP(ipStr)
if ip != nil && !ip.IsGlobalUnicast() { if ip != nil && !ip.IsGlobalUnicast() {
return nil return
} }
} }
return c.Control(func(fd uintptr) { return c.Control(func(fd uintptr) {
syscall.BindToDevice(int(fd), ifaceName) unix.BindToDevice(int(fd), ifaceName)
}) })
} }
} }
func bindIfaceToDialer(dialer *net.Dialer, ifaceName string) error { func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ net.IP) error {
dialer.Control = bindControl(ifaceName) dialer.Control = bindControl(ifaceName, dialer.Control)
return nil return nil
} }
func bindIfaceToListenConfig(lc *net.ListenConfig, ifaceName string) error { func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string) (string, error) {
lc.Control = bindControl(ifaceName) lc.Control = bindControl(ifaceName, lc.Control)
return nil return address, nil
} }

View File

@ -1,13 +1,93 @@
//go:build !linux && !darwin
// +build !linux,!darwin // +build !linux,!darwin
package dialer package dialer
import "net" import (
"net"
"strconv"
"strings"
func bindIfaceToDialer(dialer *net.Dialer, ifaceName string) error { "github.com/Dreamacro/clash/component/iface"
return errPlatformNotSupport )
func lookupLocalAddr(ifaceName string, network string, destination net.IP, port int) (net.Addr, error) {
ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil {
return nil, err
}
var addr *net.IPNet
switch network {
case "udp4", "tcp4":
addr, err = ifaceObj.PickIPv4Addr(destination)
case "tcp6", "udp6":
addr, err = ifaceObj.PickIPv6Addr(destination)
default:
if destination != nil {
if destination.To4() != nil {
addr, err = ifaceObj.PickIPv4Addr(destination)
} else {
addr, err = ifaceObj.PickIPv6Addr(destination)
}
} else {
addr, err = ifaceObj.PickIPv4Addr(destination)
}
}
if err != nil {
return nil, err
}
if strings.HasPrefix(network, "tcp") {
return &net.TCPAddr{
IP: addr.IP,
Port: port,
}, nil
} else if strings.HasPrefix(network, "udp") {
return &net.UDPAddr{
IP: addr.IP,
Port: port,
}, nil
}
return nil, iface.ErrAddrNotFound
} }
func bindIfaceToListenConfig(lc *net.ListenConfig, ifaceName string) error { func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination net.IP) error {
return errPlatformNotSupport if !destination.IsGlobalUnicast() {
return nil
}
local := int64(0)
if dialer.LocalAddr != nil {
_, port, err := net.SplitHostPort(dialer.LocalAddr.String())
if err == nil {
local, _ = strconv.ParseInt(port, 10, 16)
}
}
addr, err := lookupLocalAddr(ifaceName, network, destination, int(local))
if err != nil {
return err
}
dialer.LocalAddr = addr
return nil
}
func bindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, address string) (string, error) {
_, port, err := net.SplitHostPort(address)
if err != nil {
port = "0"
}
local, _ := strconv.ParseInt(port, 10, 16)
addr, err := lookupLocalAddr(ifaceName, network, nil, int(local))
if err != nil {
return "", err
}
return addr.String(), nil
} }

View File

@ -8,22 +8,7 @@ import (
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
) )
func Dialer() (*net.Dialer, error) { func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) {
dialer := &net.Dialer{}
if DialerHook != nil {
if err := DialerHook(dialer); err != nil {
return nil, err
}
}
return dialer, nil
}
func Dial(network, address string) (net.Conn, error) {
return DialContext(context.Background(), network, address)
}
func DialContext(ctx context.Context, network, address string) (net.Conn, error) {
switch network { switch network {
case "tcp4", "tcp6", "udp4", "udp6": case "tcp4", "tcp6", "udp4", "udp6":
host, port, err := net.SplitHostPort(address) host, port, err := net.SplitHostPort(address)
@ -31,11 +16,6 @@ func DialContext(ctx context.Context, network, address string) (net.Conn, error)
return nil, err return nil, err
} }
dialer, err := Dialer()
if err != nil {
return nil, err
}
var ip net.IP var ip net.IP
switch network { switch network {
case "tcp4", "udp4": case "tcp4", "udp4":
@ -43,38 +23,76 @@ func DialContext(ctx context.Context, network, address string) (net.Conn, error)
default: default:
ip, err = resolver.ResolveIPv6(host) ip, err = resolver.ResolveIPv6(host)
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
if DialHook != nil { return dialContext(ctx, network, ip, port, options)
if err := DialHook(dialer, network, ip); err != nil {
return nil, err
}
}
return dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), port))
case "tcp", "udp": case "tcp", "udp":
return dualStackDialContext(ctx, network, address) return dualStackDialContext(ctx, network, address, options)
default: default:
return nil, errors.New("network invalid") return nil, errors.New("network invalid")
} }
} }
func ListenPacket(network, address string) (net.PacketConn, error) { func ListenPacket(ctx context.Context, network, address string, options ...Option) (net.PacketConn, error) {
cfg := &net.ListenConfig{} cfg := &option{
if ListenPacketHook != nil { interfaceName: DefaultInterface.Load(),
var err error }
address, err = ListenPacketHook(cfg, address)
for _, o := range DefaultOptions {
o(cfg)
}
for _, o := range options {
o(cfg)
}
lc := &net.ListenConfig{}
if cfg.interfaceName != "" {
addr, err := bindIfaceToListenConfig(cfg.interfaceName, lc, network, address)
if err != nil { if err != nil {
return nil, err return nil, err
} }
address = addr
}
if cfg.addrReuse {
addrReuseToListenConfig(lc)
}
if cfg.routingMark != 0 {
bindMarkToListenConfig(cfg.routingMark, lc, network, address)
} }
return cfg.ListenPacket(context.Background(), network, address) return lc.ListenPacket(ctx, network, address)
} }
func dualStackDialContext(ctx context.Context, network, address string) (net.Conn, error) { func dialContext(ctx context.Context, network string, destination net.IP, port string, options []Option) (net.Conn, error) {
opt := &option{
interfaceName: DefaultInterface.Load(),
}
for _, o := range DefaultOptions {
o(opt)
}
for _, o := range options {
o(opt)
}
dialer := &net.Dialer{}
if opt.interfaceName != "" {
if err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil {
return nil, err
}
}
if opt.routingMark != 0 {
bindMarkToDialer(opt.routingMark, dialer, network, destination)
}
return dialer.DialContext(ctx, network, net.JoinHostPort(destination.String(), port))
}
func dualStackDialContext(ctx context.Context, network, address string, options []Option) (net.Conn, error) {
host, port, err := net.SplitHostPort(address) host, port, err := net.SplitHostPort(address)
if err != nil { if err != nil {
return nil, err return nil, err
@ -105,12 +123,6 @@ func dualStackDialContext(ctx context.Context, network, address string) (net.Con
} }
}() }()
dialer, err := Dialer()
if err != nil {
result.error = err
return
}
var ip net.IP var ip net.IP
if ipv6 { if ipv6 {
ip, result.error = resolver.ResolveIPv6(host) ip, result.error = resolver.ResolveIPv6(host)
@ -122,12 +134,7 @@ func dualStackDialContext(ctx context.Context, network, address string) (net.Con
} }
result.resolved = true result.resolved = true
if DialHook != nil { result.Conn, result.error = dialContext(ctx, network, ip, port, options)
if result.error = DialHook(dialer, network, ip); result.error != nil {
return
}
}
result.Conn, result.error = dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), port))
} }
go startRacer(ctx, network+"4", host, false) go startRacer(ctx, network+"4", host, false)

View File

@ -1,43 +0,0 @@
package dialer
import (
"errors"
"net"
)
type DialerHookFunc = func(dialer *net.Dialer) error
type DialHookFunc = func(dialer *net.Dialer, network string, ip net.IP) error
type ListenPacketHookFunc = func(lc *net.ListenConfig, address string) (string, error)
var (
DialerHook DialerHookFunc
DialHook DialHookFunc
ListenPacketHook ListenPacketHookFunc
)
var (
ErrAddrNotFound = errors.New("addr not found")
ErrNetworkNotSupport = errors.New("network not support")
)
func ListenPacketWithInterface(name string) ListenPacketHookFunc {
return func(lc *net.ListenConfig, address string) (string, error) {
err := bindIfaceToListenConfig(lc, name)
if err == errPlatformNotSupport {
address, err = fallbackBindToListenConfig(name)
}
return address, err
}
}
func DialerWithInterface(name string) DialHookFunc {
return func(dialer *net.Dialer, network string, ip net.IP) error {
err := bindIfaceToDialer(dialer, name)
if err == errPlatformNotSupport {
err = fallbackBindToDialer(dialer, network, ip, name)
}
return err
}
}

View File

@ -0,0 +1,44 @@
//go:build linux
// +build linux
package dialer
import (
"net"
"syscall"
)
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ net.IP) {
dialer.Control = bindMarkToControl(mark, dialer.Control)
}
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, address string) {
lc.Control = bindMarkToControl(mark, lc.Control)
}
func bindMarkToControl(mark int, chain controlFn) controlFn {
return func(network, address string, c syscall.RawConn) (err error) {
defer func() {
if err == nil && chain != nil {
err = chain(network, address, c)
}
}()
ipStr, _, err := net.SplitHostPort(address)
if err == nil {
ip := net.ParseIP(ipStr)
if ip != nil && !ip.IsGlobalUnicast() {
return
}
}
return c.Control(func(fd uintptr) {
switch network {
case "tcp4", "udp4":
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
case "tcp6", "udp6":
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
}
})
}
}

View File

@ -0,0 +1,27 @@
//go:build !linux
// +build !linux
package dialer
import (
"net"
"sync"
"github.com/Dreamacro/clash/log"
)
var printMarkWarnOnce sync.Once
func printMarkWarn() {
printMarkWarnOnce.Do(func() {
log.Warnln("Routing mark on socket is not supported on current platform")
})
}
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ net.IP) {
printMarkWarn()
}
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, address string) {
printMarkWarn()
}

View File

@ -0,0 +1,34 @@
package dialer
import "go.uber.org/atomic"
var (
DefaultOptions []Option
DefaultInterface = atomic.NewString("")
)
type option struct {
interfaceName string
addrReuse bool
routingMark int
}
type Option func(opt *option)
func WithInterface(name string) Option {
return func(opt *option) {
opt.interfaceName = name
}
}
func WithAddrReuse(reuse bool) Option {
return func(opt *option) {
opt.addrReuse = reuse
}
}
func WithRoutingMark(mark int) Option {
return func(opt *option) {
opt.routingMark = mark
}
}

View File

@ -0,0 +1,10 @@
//go:build !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows
// +build !darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows
package dialer
import (
"net"
)
func addrReuseToListenConfig(*net.ListenConfig) {}

View File

@ -0,0 +1,28 @@
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
// +build darwin dragonfly freebsd linux netbsd openbsd solaris
package dialer
import (
"net"
"syscall"
"golang.org/x/sys/unix"
)
func addrReuseToListenConfig(lc *net.ListenConfig) {
chain := lc.Control
lc.Control = func(network, address string, c syscall.RawConn) (err error) {
defer func() {
if err == nil && chain != nil {
err = chain(network, address, c)
}
}()
return c.Control(func(fd uintptr) {
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
})
}
}

View File

@ -0,0 +1,24 @@
package dialer
import (
"net"
"syscall"
"golang.org/x/sys/windows"
)
func addrReuseToListenConfig(lc *net.ListenConfig) {
chain := lc.Control
lc.Control = func(network, address string, c syscall.RawConn) (err error) {
defer func() {
if err == nil && chain != nil {
err = chain(network, address, c)
}
}()
return c.Control(func(fd uintptr) {
windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1)
})
}
}

View File

@ -0,0 +1,49 @@
package fakeip
import (
"net"
"github.com/Dreamacro/clash/component/profile/cachefile"
)
type cachefileStore struct {
cache *cachefile.CacheFile
}
// GetByHost implements store.GetByHost
func (c *cachefileStore) GetByHost(host string) (net.IP, bool) {
elm := c.cache.GetFakeip([]byte(host))
if elm == nil {
return nil, false
}
return net.IP(elm), true
}
// PutByHost implements store.PutByHost
func (c *cachefileStore) PutByHost(host string, ip net.IP) {
c.cache.PutFakeip([]byte(host), ip)
}
// GetByIP implements store.GetByIP
func (c *cachefileStore) GetByIP(ip net.IP) (string, bool) {
elm := c.cache.GetFakeip(ip.To4())
if elm == nil {
return "", false
}
return string(elm), true
}
// PutByIP implements store.PutByIP
func (c *cachefileStore) PutByIP(ip net.IP, host string) {
c.cache.PutFakeip(ip.To4(), []byte(host))
}
// Exist implements store.Exist
func (c *cachefileStore) Exist(ip net.IP) bool {
_, exist := c.GetByIP(ip)
return exist
}
// CloneTo implements store.CloneTo
// already persistence
func (c *cachefileStore) CloneTo(store store) {}

View File

@ -0,0 +1,60 @@
package fakeip
import (
"net"
"github.com/Dreamacro/clash/common/cache"
)
type memoryStore struct {
cache *cache.LruCache
}
// GetByHost implements store.GetByHost
func (m *memoryStore) GetByHost(host string) (net.IP, bool) {
if elm, exist := m.cache.Get(host); exist {
ip := elm.(net.IP)
// ensure ip --> host on head of linked list
m.cache.Get(ipToUint(ip.To4()))
return ip, true
}
return nil, false
}
// PutByHost implements store.PutByHost
func (m *memoryStore) PutByHost(host string, ip net.IP) {
m.cache.Set(host, ip)
}
// GetByIP implements store.GetByIP
func (m *memoryStore) GetByIP(ip net.IP) (string, bool) {
if elm, exist := m.cache.Get(ipToUint(ip.To4())); exist {
host := elm.(string)
// ensure host --> ip on head of linked list
m.cache.Get(host)
return host, true
}
return "", false
}
// PutByIP implements store.PutByIP
func (m *memoryStore) PutByIP(ip net.IP, host string) {
m.cache.Set(ipToUint(ip.To4()), host)
}
// Exist implements store.Exist
func (m *memoryStore) Exist(ip net.IP) bool {
return m.cache.Exist(ipToUint(ip.To4()))
}
// CloneTo implements store.CloneTo
// only for memoryStore to memoryStore
func (m *memoryStore) CloneTo(store store) {
if ms, ok := store.(*memoryStore); ok {
m.cache.CloneTo(ms.cache)
}
}

View File

@ -6,9 +6,19 @@ import (
"sync" "sync"
"github.com/Dreamacro/clash/common/cache" "github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/component/profile/cachefile"
"github.com/Dreamacro/clash/component/trie" "github.com/Dreamacro/clash/component/trie"
) )
type store interface {
GetByHost(host string) (net.IP, bool)
PutByHost(host string, ip net.IP)
GetByIP(ip net.IP) (string, bool)
PutByIP(ip net.IP, host string)
Exist(ip net.IP) bool
CloneTo(store)
}
// Pool is a implementation about fake ip generator without storage // Pool is a implementation about fake ip generator without storage
type Pool struct { type Pool struct {
max uint32 max uint32
@ -18,25 +28,19 @@ type Pool struct {
mux sync.Mutex mux sync.Mutex
host *trie.DomainTrie host *trie.DomainTrie
ipnet *net.IPNet ipnet *net.IPNet
cache *cache.LruCache store store
} }
// Lookup return a fake ip with host // Lookup return a fake ip with host
func (p *Pool) Lookup(host string) net.IP { func (p *Pool) Lookup(host string) net.IP {
p.mux.Lock() p.mux.Lock()
defer p.mux.Unlock() defer p.mux.Unlock()
if elm, exist := p.cache.Get(host); exist { if ip, exist := p.store.GetByHost(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 return ip
} }
ip := p.get(host) ip := p.get(host)
p.cache.Set(host, ip) p.store.PutByHost(host, ip)
return ip return ip
} }
@ -49,22 +53,11 @@ func (p *Pool) LookBack(ip net.IP) (string, bool) {
return "", false return "", false
} }
n := ipToUint(ip.To4()) return p.store.GetByIP(ip)
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
} }
// LookupHost return if domain in host // ShouldSkipped return if domain should be skipped
func (p *Pool) LookupHost(domain string) bool { func (p *Pool) ShouldSkipped(domain string) bool {
if p.host == nil { if p.host == nil {
return false return false
} }
@ -80,9 +73,7 @@ func (p *Pool) Exist(ip net.IP) bool {
return false return false
} }
n := ipToUint(ip.To4()) return p.store.Exist(ip)
offset := n - p.min + 1
return p.cache.Exist(offset)
} }
// Gateway return gateway ip // Gateway return gateway ip
@ -95,9 +86,9 @@ func (p *Pool) IPNet() *net.IPNet {
return p.ipnet return p.ipnet
} }
// PatchFrom clone cache from old pool // CloneFrom clone cache from old pool
func (p *Pool) PatchFrom(o *Pool) { func (p *Pool) CloneFrom(o *Pool) {
o.cache.CloneTo(p.cache) o.store.CloneTo(p.store)
} }
func (p *Pool) get(host string) net.IP { func (p *Pool) get(host string) net.IP {
@ -109,12 +100,13 @@ func (p *Pool) get(host string) net.IP {
break break
} }
if !p.cache.Exist(p.offset) { ip := uintToIP(p.min + p.offset - 1)
if !p.store.Exist(ip) {
break break
} }
} }
ip := uintToIP(p.min + p.offset - 1) ip := uintToIP(p.min + p.offset - 1)
p.cache.Set(p.offset, host) p.store.PutByIP(ip, host)
return ip return ip
} }
@ -130,11 +122,24 @@ func uintToIP(v uint32) net.IP {
return net.IP{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)} return net.IP{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)}
} }
// New return Pool instance type Options struct {
func New(ipnet *net.IPNet, size int, host *trie.DomainTrie) (*Pool, error) { IPNet *net.IPNet
min := ipToUint(ipnet.IP) + 2 Host *trie.DomainTrie
ones, bits := ipnet.Mask.Size() // Size sets the maximum number of entries in memory
// and does not work if Persistence is true
Size int
// Persistence will save the data to disk.
// Size will not work and record will be fully stored.
Persistence bool
}
// New return Pool instance
func New(options Options) (*Pool, error) {
min := ipToUint(options.IPNet.IP) + 2
ones, bits := options.IPNet.Mask.Size()
total := 1<<uint(bits-ones) - 2 total := 1<<uint(bits-ones) - 2
if total <= 0 { if total <= 0 {
@ -142,12 +147,22 @@ func New(ipnet *net.IPNet, size int, host *trie.DomainTrie) (*Pool, error) {
} }
max := min + uint32(total) - 1 max := min + uint32(total) - 1
return &Pool{ pool := &Pool{
min: min, min: min,
max: max, max: max,
gateway: min - 1, gateway: min - 1,
host: host, host: options.Host,
ipnet: ipnet, ipnet: options.IPNet,
cache: cache.NewLRUCache(cache.WithSize(size * 2)), }
}, nil if options.Persistence {
pool.store = &cachefileStore{
cache: cachefile.Cache(),
}
} else {
pool.store = &memoryStore{
cache: cache.NewLRUCache(cache.WithSize(options.Size * 2)),
}
}
return pool, nil
} }

View File

@ -2,38 +2,118 @@ package fakeip
import ( import (
"net" "net"
"os"
"testing" "testing"
"time"
"github.com/Dreamacro/clash/component/profile/cachefile"
"github.com/Dreamacro/clash/component/trie"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"go.etcd.io/bbolt"
) )
func createPools(options Options) ([]*Pool, string, error) {
pool, err := New(options)
if err != nil {
return nil, "", err
}
filePool, tempfile, err := createCachefileStore(options)
if err != nil {
return nil, "", err
}
return []*Pool{pool, filePool}, tempfile, nil
}
func createCachefileStore(options Options) (*Pool, string, error) {
pool, err := New(options)
if err != nil {
return nil, "", err
}
f, err := os.CreateTemp("", "clash")
if err != nil {
return nil, "", err
}
db, err := bbolt.Open(f.Name(), 0o666, &bbolt.Options{Timeout: time.Second})
if err != nil {
return nil, "", err
}
pool.store = &cachefileStore{
cache: &cachefile.CacheFile{DB: db},
}
return pool, f.Name(), nil
}
func TestPool_Basic(t *testing.T) { func TestPool_Basic(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/29") _, ipnet, _ := net.ParseCIDR("192.168.0.1/29")
pool, _ := New(ipnet, 10, nil) pools, tempfile, err := createPools(Options{
IPNet: ipnet,
Size: 10,
})
assert.Nil(t, err)
defer os.Remove(tempfile)
first := pool.Lookup("foo.com") for _, pool := range pools {
last := pool.Lookup("bar.com") first := pool.Lookup("foo.com")
bar, exist := pool.LookBack(last) last := pool.Lookup("bar.com")
bar, exist := pool.LookBack(last)
assert.True(t, first.Equal(net.IP{192, 168, 0, 2})) assert.True(t, first.Equal(net.IP{192, 168, 0, 2}))
assert.True(t, last.Equal(net.IP{192, 168, 0, 3})) assert.Equal(t, pool.Lookup("foo.com"), net.IP{192, 168, 0, 2})
assert.True(t, exist) assert.True(t, last.Equal(net.IP{192, 168, 0, 3}))
assert.Equal(t, bar, "bar.com") assert.True(t, exist)
assert.Equal(t, bar, "bar.com")
assert.Equal(t, pool.Gateway(), net.IP{192, 168, 0, 1})
assert.Equal(t, pool.IPNet().String(), ipnet.String())
assert.True(t, pool.Exist(net.IP{192, 168, 0, 3}))
assert.False(t, pool.Exist(net.IP{192, 168, 0, 4}))
assert.False(t, pool.Exist(net.ParseIP("::1")))
}
} }
func TestPool_Cycle(t *testing.T) { func TestPool_CycleUsed(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/30") _, ipnet, _ := net.ParseCIDR("192.168.0.1/30")
pool, _ := New(ipnet, 10, nil) pools, tempfile, err := createPools(Options{
IPNet: ipnet,
Size: 10,
})
assert.Nil(t, err)
defer os.Remove(tempfile)
first := pool.Lookup("foo.com") for _, pool := range pools {
same := pool.Lookup("baz.com") first := pool.Lookup("foo.com")
same := pool.Lookup("baz.com")
assert.True(t, first.Equal(same))
}
}
assert.True(t, first.Equal(same)) func TestPool_Skip(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/30")
tree := trie.New()
tree.Insert("example.com", tree)
pools, tempfile, err := createPools(Options{
IPNet: ipnet,
Size: 10,
Host: tree,
})
assert.Nil(t, err)
defer os.Remove(tempfile)
for _, pool := range pools {
assert.True(t, pool.ShouldSkipped("example.com"))
assert.False(t, pool.ShouldSkipped("foo.com"))
}
} }
func TestPool_MaxCacheSize(t *testing.T) { func TestPool_MaxCacheSize(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24") _, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
pool, _ := New(ipnet, 2, nil) pool, _ := New(Options{
IPNet: ipnet,
Size: 2,
})
first := pool.Lookup("foo.com") first := pool.Lookup("foo.com")
pool.Lookup("bar.com") pool.Lookup("bar.com")
@ -45,7 +125,10 @@ func TestPool_MaxCacheSize(t *testing.T) {
func TestPool_DoubleMapping(t *testing.T) { func TestPool_DoubleMapping(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24") _, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
pool, _ := New(ipnet, 2, nil) pool, _ := New(Options{
IPNet: ipnet,
Size: 2,
})
// fill cache // fill cache
fooIP := pool.Lookup("foo.com") fooIP := pool.Lookup("foo.com")
@ -70,9 +153,35 @@ func TestPool_DoubleMapping(t *testing.T) {
assert.False(t, bazIP.Equal(newBazIP)) assert.False(t, bazIP.Equal(newBazIP))
} }
func TestPool_Clone(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
pool, _ := New(Options{
IPNet: ipnet,
Size: 2,
})
first := pool.Lookup("foo.com")
last := pool.Lookup("bar.com")
assert.True(t, first.Equal(net.IP{192, 168, 0, 2}))
assert.True(t, last.Equal(net.IP{192, 168, 0, 3}))
newPool, _ := New(Options{
IPNet: ipnet,
Size: 2,
})
newPool.CloneFrom(pool)
_, firstExist := newPool.LookBack(first)
_, lastExist := newPool.LookBack(last)
assert.True(t, firstExist)
assert.True(t, lastExist)
}
func TestPool_Error(t *testing.T) { func TestPool_Error(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/31") _, ipnet, _ := net.ParseCIDR("192.168.0.1/31")
_, err := New(ipnet, 10, nil) _, err := New(Options{
IPNet: ipnet,
Size: 10,
})
assert.Error(t, err) assert.Error(t, err)
} }

115
component/iface/iface.go Normal file
View File

@ -0,0 +1,115 @@
package iface
import (
"errors"
"net"
"time"
"github.com/Dreamacro/clash/common/singledo"
)
type Interface struct {
Index int
Name string
Addrs []*net.IPNet
HardwareAddr net.HardwareAddr
}
var (
ErrIfaceNotFound = errors.New("interface not found")
ErrAddrNotFound = errors.New("addr not found")
)
var interfaces = singledo.NewSingle(time.Second * 20)
func ResolveInterface(name string) (*Interface, error) {
value, err, _ := interfaces.Do(func() (interface{}, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}
r := map[string]*Interface{}
for _, iface := range ifaces {
addrs, err := iface.Addrs()
if err != nil {
continue
}
ipNets := make([]*net.IPNet, 0, len(addrs))
for _, addr := range addrs {
ipNet := addr.(*net.IPNet)
if v4 := ipNet.IP.To4(); v4 != nil {
ipNet.IP = v4
}
ipNets = append(ipNets, ipNet)
}
r[iface.Name] = &Interface{
Index: iface.Index,
Name: iface.Name,
Addrs: ipNets,
HardwareAddr: iface.HardwareAddr,
}
}
return r, nil
})
if err != nil {
return nil, err
}
ifaces := value.(map[string]*Interface)
iface, ok := ifaces[name]
if !ok {
return nil, ErrIfaceNotFound
}
return iface, nil
}
func FlushCache() {
interfaces.Reset()
}
func (iface *Interface) PickIPv4Addr(destination net.IP) (*net.IPNet, error) {
return iface.pickIPAddr(destination, func(addr *net.IPNet) bool {
return addr.IP.To4() != nil
})
}
func (iface *Interface) PickIPv6Addr(destination net.IP) (*net.IPNet, error) {
return iface.pickIPAddr(destination, func(addr *net.IPNet) bool {
return addr.IP.To4() == nil
})
}
func (iface *Interface) pickIPAddr(destination net.IP, accept func(addr *net.IPNet) bool) (*net.IPNet, error) {
var fallback *net.IPNet
for _, addr := range iface.Addrs {
if !accept(addr) {
continue
}
if fallback == nil && !addr.IP.IsLinkLocalUnicast() {
fallback = addr
if destination == nil {
break
}
}
if destination != nil && addr.Contains(destination) {
return addr, nil
}
}
if fallback == nil {
return nil, ErrAddrNotFound
}
return fallback, nil
}

View File

@ -9,8 +9,10 @@ import (
"github.com/oschwald/geoip2-golang" "github.com/oschwald/geoip2-golang"
) )
var mmdb *geoip2.Reader var (
var once sync.Once mmdb *geoip2.Reader
once sync.Once
)
func LoadFromBytes(buffer []byte) { func LoadFromBytes(buffer []byte) {
once.Do(func() { once.Do(func() {

View File

@ -1,12 +1,13 @@
package process package process
import ( import (
"bytes"
"encoding/binary" "encoding/binary"
"net" "net"
"path/filepath" "path/filepath"
"syscall" "syscall"
"unsafe" "unsafe"
"golang.org/x/sys/unix"
) )
const ( const (
@ -94,12 +95,8 @@ func getExecPathFromPID(pid uint32) (string, error) {
if errno != 0 { if errno != 0 {
return "", errno return "", errno
} }
firstZero := bytes.IndexByte(buf, 0)
if firstZero <= 0 {
return "", nil
}
return filepath.Base(string(buf[:firstZero])), nil return filepath.Base(unix.ByteSliceToString(buf)), nil
} }
func readNativeUint32(b []byte) uint32 { func readNativeUint32(b []byte) uint32 {

View File

@ -5,8 +5,8 @@ import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net" "net"
"os"
"path" "path"
"path/filepath" "path/filepath"
"syscall" "syscall"
@ -16,17 +16,19 @@ import (
) )
// from https://github.com/vishvananda/netlink/blob/bca67dfc8220b44ef582c9da4e9172bf1c9ec973/nl/nl_linux.go#L52-L62 // from https://github.com/vishvananda/netlink/blob/bca67dfc8220b44ef582c9da4e9172bf1c9ec973/nl/nl_linux.go#L52-L62
func init() { var nativeEndian = func() binary.ByteOrder {
var x uint32 = 0x01020304 var x uint32 = 0x01020304
if *(*byte)(unsafe.Pointer(&x)) == 0x01 { if *(*byte)(unsafe.Pointer(&x)) == 0x01 {
nativeEndian = binary.BigEndian return binary.BigEndian
} else {
nativeEndian = binary.LittleEndian
} }
}
type SocketResolver func(network string, ip net.IP, srcPort int) (inode, uid int, err error) return binary.LittleEndian
type ProcessNameResolver func(inode, uid int) (name string, err error) }()
type (
SocketResolver func(network string, ip net.IP, srcPort int) (inode, uid int, err error)
ProcessNameResolver func(inode, uid int) (name string, err error)
)
// export for android // export for android
var ( var (
@ -40,8 +42,6 @@ const (
pathProc = "/proc" pathProc = "/proc"
) )
var nativeEndian binary.ByteOrder = binary.LittleEndian
func findProcessName(network string, ip net.IP, srcPort int) (string, error) { func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
inode, uid, err := DefaultSocketResolver(network, ip, srcPort) inode, uid, err := DefaultSocketResolver(network, ip, srcPort)
if err != nil { if err != nil {
@ -169,7 +169,7 @@ func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) {
} }
func resolveProcessNameByProcSearch(inode, uid int) (string, error) { func resolveProcessNameByProcSearch(inode, uid int) (string, error) {
files, err := ioutil.ReadDir(pathProc) files, err := os.ReadDir(pathProc)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -182,14 +182,18 @@ func resolveProcessNameByProcSearch(inode, uid int) (string, error) {
continue continue
} }
if f.Sys().(*syscall.Stat_t).Uid != uint32(uid) { info, err := f.Info()
if err != nil {
return "", err
}
if info.Sys().(*syscall.Stat_t).Uid != uint32(uid) {
continue continue
} }
processPath := path.Join(pathProc, f.Name()) processPath := path.Join(pathProc, f.Name())
fdPath := path.Join(processPath, "fd") fdPath := path.Join(processPath, "fd")
fds, err := ioutil.ReadDir(fdPath) fds, err := os.ReadDir(fdPath)
if err != nil { if err != nil {
continue continue
} }
@ -201,7 +205,7 @@ func resolveProcessNameByProcSearch(inode, uid int) (string, error) {
} }
if bytes.Equal(buffer[:n], socket) { if bytes.Equal(buffer[:n], socket) {
cmdline, err := ioutil.ReadFile(path.Join(processPath, "cmdline")) cmdline, err := os.ReadFile(path.Join(processPath, "cmdline"))
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -1,4 +1,7 @@
// +build !darwin,!linux,!windows //go:build !darwin && !linux && !windows && (!freebsd || !amd64)
// +build !darwin
// +build !linux
// +build !windows
// +build !freebsd !amd64 // +build !freebsd !amd64
package process package process

View File

@ -3,52 +3,47 @@ package cachefile
import ( import (
"bytes" "bytes"
"encoding/gob" "encoding/gob"
"io/ioutil"
"os" "os"
"sync" "sync"
"time"
"github.com/Dreamacro/clash/component/profile" "github.com/Dreamacro/clash/component/profile"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
"go.etcd.io/bbolt"
) )
var ( var (
initOnce sync.Once initOnce sync.Once
fileMode os.FileMode = 0666 fileMode os.FileMode = 0o666
defaultCache *CacheFile defaultCache *CacheFile
)
type cache struct { bucketSelected = []byte("selected")
Selected map[string]string bucketFakeip = []byte("fakeip")
} )
// CacheFile store and update the cache file // CacheFile store and update the cache file
type CacheFile struct { type CacheFile struct {
path string DB *bbolt.DB
model *cache
buf *bytes.Buffer
mux sync.Mutex
} }
func (c *CacheFile) SetSelected(group, selected string) { func (c *CacheFile) SetSelected(group, selected string) {
if !profile.StoreSelected.Load() { if !profile.StoreSelected.Load() {
return return
} } else if c.DB == nil {
c.mux.Lock()
defer c.mux.Unlock()
model := c.element()
model.Selected[group] = selected
c.buf.Reset()
if err := gob.NewEncoder(c.buf).Encode(model); err != nil {
log.Warnln("[CacheFile] encode gob failed: %s", err.Error())
return return
} }
if err := ioutil.WriteFile(c.path, c.buf.Bytes(), fileMode); err != nil { err := c.DB.Batch(func(t *bbolt.Tx) error {
log.Warnln("[CacheFile] write cache to %s failed: %s", c.path, err.Error()) bucket, err := t.CreateBucketIfNotExists(bucketSelected)
if err != nil {
return err
}
return bucket.Put([]byte(group), []byte(selected))
})
if err != nil {
log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error())
return return
} }
} }
@ -56,46 +51,131 @@ func (c *CacheFile) SetSelected(group, selected string) {
func (c *CacheFile) SelectedMap() map[string]string { func (c *CacheFile) SelectedMap() map[string]string {
if !profile.StoreSelected.Load() { if !profile.StoreSelected.Load() {
return nil return nil
} else if c.DB == nil {
return nil
} }
c.mux.Lock()
defer c.mux.Unlock()
model := c.element()
mapping := map[string]string{} mapping := map[string]string{}
for k, v := range model.Selected { c.DB.View(func(t *bbolt.Tx) error {
mapping[k] = v bucket := t.Bucket(bucketSelected)
} if bucket == nil {
return nil
}
c := bucket.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
mapping[string(k)] = string(v)
}
return nil
})
return mapping return mapping
} }
func (c *CacheFile) element() *cache { func (c *CacheFile) PutFakeip(key, value []byte) error {
if c.model != nil { if c.DB == nil {
return c.model return nil
} }
err := c.DB.Batch(func(t *bbolt.Tx) error {
bucket, err := t.CreateBucketIfNotExists(bucketFakeip)
if err != nil {
return err
}
return bucket.Put(key, value)
})
if err != nil {
log.Warnln("[CacheFile] write cache to %s failed: %s", c.DB.Path(), err.Error())
}
return err
}
func (c *CacheFile) GetFakeip(key []byte) []byte {
if c.DB == nil {
return nil
}
tx, err := c.DB.Begin(false)
if err != nil {
return nil
}
defer tx.Rollback()
bucket := tx.Bucket(bucketFakeip)
if bucket == nil {
return nil
}
return bucket.Get(key)
}
func (c *CacheFile) Close() error {
return c.DB.Close()
}
// TODO: remove migrateCache until 2022
func migrateCache() {
defer func() {
options := bbolt.Options{Timeout: time.Second}
db, err := bbolt.Open(C.Path.Cache(), fileMode, &options)
switch err {
case bbolt.ErrInvalid, bbolt.ErrChecksum, bbolt.ErrVersionMismatch:
if err = os.Remove(C.Path.Cache()); err != nil {
log.Warnln("[CacheFile] remove invalid cache file error: %s", err.Error())
break
}
log.Infoln("[CacheFile] remove invalid cache file and create new one")
db, err = bbolt.Open(C.Path.Cache(), fileMode, &options)
}
if err != nil {
log.Warnln("[CacheFile] can't open cache file: %s", err.Error())
}
defaultCache = &CacheFile{
DB: db,
}
}()
buf, err := os.ReadFile(C.Path.OldCache())
if err != nil {
return
}
defer os.Remove(C.Path.OldCache())
// read old cache file
type cache struct {
Selected map[string]string
}
model := &cache{ model := &cache{
Selected: map[string]string{}, Selected: map[string]string{},
} }
bufReader := bytes.NewBuffer(buf)
gob.NewDecoder(bufReader).Decode(model)
if buf, err := ioutil.ReadFile(c.path); err == nil { // write to new cache file
bufReader := bytes.NewBuffer(buf) db, err := bbolt.Open(C.Path.Cache(), fileMode, nil)
gob.NewDecoder(bufReader).Decode(model) if err != nil {
return
} }
defer db.Close()
c.model = model db.Batch(func(t *bbolt.Tx) error {
return c.model bucket, err := t.CreateBucketIfNotExists(bucketSelected)
if err != nil {
return err
}
for group, selected := range model.Selected {
if err := bucket.Put([]byte(group), []byte(selected)); err != nil {
return err
}
}
return nil
})
} }
// Cache return singleton of CacheFile // Cache return singleton of CacheFile
func Cache() *CacheFile { func Cache() *CacheFile {
initOnce.Do(func() { initOnce.Do(migrateCache)
defaultCache = &CacheFile{
path: C.Path.Cache(),
buf: &bytes.Buffer{},
}
})
return defaultCache return defaultCache
} }

View File

@ -4,7 +4,5 @@ import (
"go.uber.org/atomic" "go.uber.org/atomic"
) )
var ( // StoreSelected is a global switch for storing selected proxy to cache
// StoreSelected is a global switch for storing selected proxy to cache var StoreSelected = atomic.NewBool(true)
StoreSelected = atomic.NewBool(true)
)

View File

@ -1,18 +0,0 @@
package tools
import (
"bytes"
"math/rand"
"sync"
"github.com/Dreamacro/clash/common/pool"
)
var BufPool = sync.Pool{New: func() interface{} { return &bytes.Buffer{} }}
func AppendRandBytes(b *bytes.Buffer, length int) {
randBytes := pool.Get(length)
defer pool.Put(randBytes)
rand.Read(randBytes)
b.Write(randBytes)
}

View File

@ -12,10 +12,8 @@ const (
domainStep = "." domainStep = "."
) )
var ( // ErrInvalidDomain means insert domain is invalid
// ErrInvalidDomain means insert domain is invalid var ErrInvalidDomain = errors.New("invalid domain")
ErrInvalidDomain = errors.New("invalid domain")
)
// DomainTrie contains the main logic for adding and searching nodes for domain segments. // DomainTrie contains the main logic for adding and searching nodes for domain segments.
// support wildcard domain (e.g *.google.com) // support wildcard domain (e.g *.google.com)
@ -23,7 +21,7 @@ type DomainTrie struct {
root *Node root *Node
} }
func validAndSplitDomain(domain string) ([]string, bool) { func ValidAndSplitDomain(domain string) ([]string, bool) {
if domain != "" && domain[len(domain)-1] == '.' { if domain != "" && domain[len(domain)-1] == '.' {
return nil, false return nil, false
} }
@ -54,7 +52,7 @@ func validAndSplitDomain(domain string) ([]string, bool) {
// 4. .example.com // 4. .example.com
// 5. +.example.com // 5. +.example.com
func (t *DomainTrie) Insert(domain string, data interface{}) error { func (t *DomainTrie) Insert(domain string, data interface{}) error {
parts, valid := validAndSplitDomain(domain) parts, valid := ValidAndSplitDomain(domain)
if !valid { if !valid {
return ErrInvalidDomain return ErrInvalidDomain
} }
@ -91,7 +89,7 @@ func (t *DomainTrie) insert(parts []string, data interface{}) {
// 2. wildcard domain // 2. wildcard domain
// 2. dot wildcard domain // 2. dot wildcard domain
func (t *DomainTrie) Search(domain string) *Node { func (t *DomainTrie) Search(domain string) *Node {
parts, valid := validAndSplitDomain(domain) parts, valid := ValidAndSplitDomain(domain)
if !valid || parts[0] == "" { if !valid || parts[0] == "" {
return nil return nil
} }

View File

@ -1,169 +0,0 @@
package vmess
import (
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/gorilla/websocket"
)
type websocketConn struct {
conn *websocket.Conn
reader io.Reader
remoteAddr net.Addr
// https://godoc.org/github.com/gorilla/websocket#hdr-Concurrency
rMux sync.Mutex
wMux sync.Mutex
}
type WebsocketConfig struct {
Host string
Port string
Path string
Headers http.Header
TLS bool
SkipCertVerify bool
ServerName string
SessionCache tls.ClientSessionCache
}
// Read implements net.Conn.Read()
func (wsc *websocketConn) Read(b []byte) (int, error) {
wsc.rMux.Lock()
defer wsc.rMux.Unlock()
for {
reader, err := wsc.getReader()
if err != nil {
return 0, err
}
nBytes, err := reader.Read(b)
if err == io.EOF {
wsc.reader = nil
continue
}
return nBytes, err
}
}
// Write implements io.Writer.
func (wsc *websocketConn) Write(b []byte) (int, error) {
wsc.wMux.Lock()
defer wsc.wMux.Unlock()
if err := wsc.conn.WriteMessage(websocket.BinaryMessage, b); err != nil {
return 0, err
}
return len(b), nil
}
func (wsc *websocketConn) Close() error {
var errors []string
if err := wsc.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Now().Add(time.Second*5)); err != nil {
errors = append(errors, err.Error())
}
if err := wsc.conn.Close(); err != nil {
errors = append(errors, err.Error())
}
if len(errors) > 0 {
return fmt.Errorf("failed to close connection: %s", strings.Join(errors, ","))
}
return nil
}
func (wsc *websocketConn) getReader() (io.Reader, error) {
if wsc.reader != nil {
return wsc.reader, nil
}
_, reader, err := wsc.conn.NextReader()
if err != nil {
return nil, err
}
wsc.reader = reader
return reader, nil
}
func (wsc *websocketConn) LocalAddr() net.Addr {
return wsc.conn.LocalAddr()
}
func (wsc *websocketConn) RemoteAddr() net.Addr {
return wsc.remoteAddr
}
func (wsc *websocketConn) SetDeadline(t time.Time) error {
if err := wsc.SetReadDeadline(t); err != nil {
return err
}
return wsc.SetWriteDeadline(t)
}
func (wsc *websocketConn) SetReadDeadline(t time.Time) error {
return wsc.conn.SetReadDeadline(t)
}
func (wsc *websocketConn) SetWriteDeadline(t time.Time) error {
return wsc.conn.SetWriteDeadline(t)
}
func StreamWebsocketConn(conn net.Conn, c *WebsocketConfig) (net.Conn, error) {
dialer := &websocket.Dialer{
NetDial: func(network, addr string) (net.Conn, error) {
return conn, nil
},
ReadBufferSize: 4 * 1024,
WriteBufferSize: 4 * 1024,
HandshakeTimeout: time.Second * 8,
}
scheme := "ws"
if c.TLS {
scheme = "wss"
dialer.TLSClientConfig = &tls.Config{
ServerName: c.Host,
InsecureSkipVerify: c.SkipCertVerify,
ClientSessionCache: c.SessionCache,
}
if c.ServerName != "" {
dialer.TLSClientConfig.ServerName = c.ServerName
} else if host := c.Headers.Get("Host"); host != "" {
dialer.TLSClientConfig.ServerName = host
}
}
uri := url.URL{
Scheme: scheme,
Host: net.JoinHostPort(c.Host, c.Port),
Path: c.Path,
}
headers := http.Header{}
if c.Headers != nil {
for k := range c.Headers {
headers.Add(k, c.Headers.Get(k))
}
}
wsConn, resp, err := dialer.Dial(uri.String(), headers)
if err != nil {
reason := err.Error()
if resp != nil {
reason = resp.Status
}
return nil, fmt.Errorf("dial %s error: %s", uri.Host, reason)
}
return &websocketConn{
conn: wsConn,
remoteAddr: conn.RemoteAddr(),
}, nil
}

View File

@ -8,19 +8,21 @@ import (
"os" "os"
"strings" "strings"
"github.com/Dreamacro/clash/adapters/outbound" "github.com/Dreamacro/clash/adapter"
"github.com/Dreamacro/clash/adapters/outboundgroup" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapters/provider" "github.com/Dreamacro/clash/adapter/outboundgroup"
"github.com/Dreamacro/clash/adapter/provider"
"github.com/Dreamacro/clash/component/auth" "github.com/Dreamacro/clash/component/auth"
"github.com/Dreamacro/clash/component/fakeip" "github.com/Dreamacro/clash/component/fakeip"
"github.com/Dreamacro/clash/component/trie" "github.com/Dreamacro/clash/component/trie"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
providerTypes "github.com/Dreamacro/clash/constant/provider"
"github.com/Dreamacro/clash/dns" "github.com/Dreamacro/clash/dns"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
R "github.com/Dreamacro/clash/rules" R "github.com/Dreamacro/clash/rule"
T "github.com/Dreamacro/clash/tunnel" T "github.com/Dreamacro/clash/tunnel"
yaml "gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
// General config // General config
@ -60,22 +62,25 @@ type DNS struct {
Fallback []dns.NameServer `yaml:"fallback"` Fallback []dns.NameServer `yaml:"fallback"`
FallbackFilter FallbackFilter `yaml:"fallback-filter"` FallbackFilter FallbackFilter `yaml:"fallback-filter"`
Listen string `yaml:"listen"` Listen string `yaml:"listen"`
EnhancedMode dns.EnhancedMode `yaml:"enhanced-mode"` EnhancedMode C.DNSMode `yaml:"enhanced-mode"`
DefaultNameserver []dns.NameServer `yaml:"default-nameserver"` DefaultNameserver []dns.NameServer `yaml:"default-nameserver"`
FakeIPRange *fakeip.Pool FakeIPRange *fakeip.Pool
Hosts *trie.DomainTrie Hosts *trie.DomainTrie
NameServerPolicy map[string]dns.NameServer
} }
// FallbackFilter config // FallbackFilter config
type FallbackFilter struct { type FallbackFilter struct {
GeoIP bool `yaml:"geoip"` GeoIP bool `yaml:"geoip"`
IPCIDR []*net.IPNet `yaml:"ipcidr"` GeoIPCode string `yaml:"geoip-code"`
Domain []string `yaml:"domain"` IPCIDR []*net.IPNet `yaml:"ipcidr"`
Domain []string `yaml:"domain"`
} }
// Profile config // Profile config
type Profile struct { type Profile struct {
StoreSelected bool `yaml:"store-selected"` StoreSelected bool `yaml:"store-selected"`
StoreFakeIP bool `yaml:"store-fake-ip"`
} }
// Experimental config // Experimental config
@ -91,7 +96,7 @@ type Config struct {
Rules []C.Rule Rules []C.Rule
Users []auth.AuthUser Users []auth.AuthUser
Proxies map[string]C.Proxy Proxies map[string]C.Proxy
Providers map[string]provider.ProxyProvider Providers map[string]providerTypes.ProxyProvider
} }
type RawDNS struct { type RawDNS struct {
@ -102,16 +107,18 @@ type RawDNS struct {
Fallback []string `yaml:"fallback"` Fallback []string `yaml:"fallback"`
FallbackFilter RawFallbackFilter `yaml:"fallback-filter"` FallbackFilter RawFallbackFilter `yaml:"fallback-filter"`
Listen string `yaml:"listen"` Listen string `yaml:"listen"`
EnhancedMode dns.EnhancedMode `yaml:"enhanced-mode"` EnhancedMode C.DNSMode `yaml:"enhanced-mode"`
FakeIPRange string `yaml:"fake-ip-range"` FakeIPRange string `yaml:"fake-ip-range"`
FakeIPFilter []string `yaml:"fake-ip-filter"` FakeIPFilter []string `yaml:"fake-ip-filter"`
DefaultNameserver []string `yaml:"default-nameserver"` DefaultNameserver []string `yaml:"default-nameserver"`
NameServerPolicy map[string]string `yaml:"nameserver-policy"`
} }
type RawFallbackFilter struct { type RawFallbackFilter struct {
GeoIP bool `yaml:"geoip"` GeoIP bool `yaml:"geoip"`
IPCIDR []string `yaml:"ipcidr"` GeoIPCode string `yaml:"geoip-code"`
Domain []string `yaml:"domain"` IPCIDR []string `yaml:"ipcidr"`
Domain []string `yaml:"domain"`
} }
type RawConfig struct { type RawConfig struct {
@ -168,8 +175,9 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
UseHosts: true, UseHosts: true,
FakeIPRange: "198.18.0.1/16", FakeIPRange: "198.18.0.1/16",
FallbackFilter: RawFallbackFilter{ FallbackFilter: RawFallbackFilter{
GeoIP: true, GeoIP: true,
IPCIDR: []string{}, GeoIPCode: "CN",
IPCIDR: []string{},
}, },
DefaultNameserver: []string{ DefaultNameserver: []string{
"114.114.114.114", "114.114.114.114",
@ -181,7 +189,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
}, },
} }
if err := yaml.Unmarshal(buf, &rawCfg); err != nil { if err := yaml.Unmarshal(buf, rawCfg); err != nil {
return nil, err return nil, err
} }
@ -219,7 +227,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
} }
config.Hosts = hosts config.Hosts = hosts
dnsCfg, err := parseDNS(rawCfg.DNS, hosts) dnsCfg, err := parseDNS(rawCfg, hosts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -264,21 +272,21 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
}, nil }, nil
} }
func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[string]provider.ProxyProvider, err error) { func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[string]providerTypes.ProxyProvider, err error) {
proxies = make(map[string]C.Proxy) proxies = make(map[string]C.Proxy)
providersMap = make(map[string]provider.ProxyProvider) providersMap = make(map[string]providerTypes.ProxyProvider)
proxyList := []string{} proxyList := []string{}
proxiesConfig := cfg.Proxy proxiesConfig := cfg.Proxy
groupsConfig := cfg.ProxyGroup groupsConfig := cfg.ProxyGroup
providersConfig := cfg.ProxyProvider providersConfig := cfg.ProxyProvider
proxies["DIRECT"] = outbound.NewProxy(outbound.NewDirect()) proxies["DIRECT"] = adapter.NewProxy(outbound.NewDirect())
proxies["REJECT"] = outbound.NewProxy(outbound.NewReject()) proxies["REJECT"] = adapter.NewProxy(outbound.NewReject())
proxyList = append(proxyList, "DIRECT", "REJECT") proxyList = append(proxyList, "DIRECT", "REJECT")
// parse proxy // parse proxy
for idx, mapping := range proxiesConfig { for idx, mapping := range proxiesConfig {
proxy, err := outbound.ParseProxy(mapping) proxy, err := adapter.ParseProxy(mapping)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("proxy %d: %w", idx, err) return nil, nil, fmt.Errorf("proxy %d: %w", idx, err)
} }
@ -337,12 +345,12 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
return nil, nil, fmt.Errorf("proxy group %s: the duplicate name", groupName) return nil, nil, fmt.Errorf("proxy group %s: the duplicate name", groupName)
} }
proxies[groupName] = outbound.NewProxy(group) proxies[groupName] = adapter.NewProxy(group)
} }
// initial compatible provider // initial compatible provider
for _, pd := range providersMap { for _, pd := range providersMap {
if pd.VehicleType() != provider.Compatible { if pd.VehicleType() != providerTypes.Compatible {
continue continue
} }
@ -364,9 +372,9 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
&outboundgroup.GroupCommonOption{ &outboundgroup.GroupCommonOption{
Name: "GLOBAL", Name: "GLOBAL",
}, },
[]provider.ProxyProvider{pd}, []providerTypes.ProxyProvider{pd},
) )
proxies["GLOBAL"] = outbound.NewProxy(global) proxies["GLOBAL"] = adapter.NewProxy(global)
return proxies, providersMap, nil return proxies, providersMap, nil
} }
@ -481,6 +489,9 @@ func parseNameServer(servers []string) ([]dns.NameServer, error) {
clearURL := url.URL{Scheme: "https", Host: u.Host, Path: u.Path} clearURL := url.URL{Scheme: "https", Host: u.Host, Path: u.Path}
addr = clearURL.String() addr = clearURL.String()
dnsNetType = "https" // DNS over HTTPS dnsNetType = "https" // DNS over HTTPS
case "dhcp":
addr = u.Host
dnsNetType = "dhcp" // UDP from DHCP
default: default:
return nil, fmt.Errorf("DNS NameServer[%d] unsupport scheme: %s", idx, u.Scheme) return nil, fmt.Errorf("DNS NameServer[%d] unsupport scheme: %s", idx, u.Scheme)
} }
@ -500,6 +511,23 @@ func parseNameServer(servers []string) ([]dns.NameServer, error) {
return nameservers, nil return nameservers, nil
} }
func parseNameServerPolicy(nsPolicy map[string]string) (map[string]dns.NameServer, error) {
policy := map[string]dns.NameServer{}
for domain, server := range nsPolicy {
nameservers, err := parseNameServer([]string{server})
if err != nil {
return nil, err
}
if _, valid := trie.ValidAndSplitDomain(domain); !valid {
return nil, fmt.Errorf("DNS ResoverRule invalid domain: %s", domain)
}
policy[domain] = nameservers[0]
}
return policy, nil
}
func parseFallbackIPCIDR(ips []string) ([]*net.IPNet, error) { func parseFallbackIPCIDR(ips []string) ([]*net.IPNet, error) {
ipNets := []*net.IPNet{} ipNets := []*net.IPNet{}
@ -514,7 +542,8 @@ func parseFallbackIPCIDR(ips []string) ([]*net.IPNet, error) {
return ipNets, nil return ipNets, nil
} }
func parseDNS(cfg RawDNS, hosts *trie.DomainTrie) (*DNS, error) { func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie) (*DNS, error) {
cfg := rawCfg.DNS
if cfg.Enable && len(cfg.NameServer) == 0 { if cfg.Enable && len(cfg.NameServer) == 0 {
return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty") return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty")
} }
@ -537,6 +566,10 @@ func parseDNS(cfg RawDNS, hosts *trie.DomainTrie) (*DNS, error) {
return nil, err return nil, err
} }
if dnsCfg.NameServerPolicy, err = parseNameServerPolicy(cfg.NameServerPolicy); err != nil {
return nil, err
}
if len(cfg.DefaultNameserver) == 0 { if len(cfg.DefaultNameserver) == 0 {
return nil, errors.New("default nameserver should have at least one nameserver") return nil, errors.New("default nameserver should have at least one nameserver")
} }
@ -551,7 +584,7 @@ func parseDNS(cfg RawDNS, hosts *trie.DomainTrie) (*DNS, error) {
} }
} }
if cfg.EnhancedMode == dns.FAKEIP { if cfg.EnhancedMode == C.DNSFakeIP {
_, ipnet, err := net.ParseCIDR(cfg.FakeIPRange) _, ipnet, err := net.ParseCIDR(cfg.FakeIPRange)
if err != nil { if err != nil {
return nil, err return nil, err
@ -566,7 +599,12 @@ func parseDNS(cfg RawDNS, hosts *trie.DomainTrie) (*DNS, error) {
} }
} }
pool, err := fakeip.New(ipnet, 1000, host) pool, err := fakeip.New(fakeip.Options{
IPNet: ipnet,
Size: 1000,
Host: host,
Persistence: rawCfg.Profile.StoreFakeIP,
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -575,6 +613,7 @@ func parseDNS(cfg RawDNS, hosts *trie.DomainTrie) (*DNS, error) {
} }
dnsCfg.FallbackFilter.GeoIP = cfg.FallbackFilter.GeoIP dnsCfg.FallbackFilter.GeoIP = cfg.FallbackFilter.GeoIP
dnsCfg.FallbackFilter.GeoIPCode = cfg.FallbackFilter.GeoIPCode
if fallbackip, err := parseFallbackIPCIDR(cfg.FallbackFilter.IPCIDR); err == nil { if fallbackip, err := parseFallbackIPCIDR(cfg.FallbackFilter.IPCIDR); err == nil {
dnsCfg.FallbackFilter.IPCIDR = fallbackip dnsCfg.FallbackFilter.IPCIDR = fallbackip
} }

View File

@ -18,7 +18,7 @@ func downloadMMDB(path string) (err error) {
} }
defer resp.Body.Close() defer resp.Body.Close()
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil { if err != nil {
return err return err
} }
@ -54,7 +54,7 @@ func initMMDB() error {
func Init(dir string) error { func Init(dir string) error {
// initial homedir // initial homedir
if _, err := os.Stat(dir); os.IsNotExist(err) { if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, 0777); err != nil { if err := os.MkdirAll(dir, 0o777); err != nil {
return fmt.Errorf("can't create config directory %s: %s", dir, err.Error()) return fmt.Errorf("can't create config directory %s: %s", dir, err.Error())
} }
} }
@ -62,7 +62,7 @@ func Init(dir string) error {
// initial config.yaml // initial config.yaml
if _, err := os.Stat(C.Path.Config()); os.IsNotExist(err) { if _, err := os.Stat(C.Path.Config()); os.IsNotExist(err) {
log.Infoln("Can't find config, create a initial config file") log.Infoln("Can't find config, create a initial config file")
f, err := os.OpenFile(C.Path.Config(), os.O_CREATE|os.O_WRONLY, 0644) f, err := os.OpenFile(C.Path.Config(), os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil { if err != nil {
return fmt.Errorf("can't create file %s: %s", C.Path.Config(), err.Error()) return fmt.Errorf("can't create file %s: %s", C.Path.Config(), err.Error())
} }

View File

@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/Dreamacro/clash/adapters/outboundgroup" "github.com/Dreamacro/clash/adapter/outboundgroup"
"github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/common/structure"
) )

View File

@ -5,6 +5,8 @@ import (
"fmt" "fmt"
"net" "net"
"time" "time"
"github.com/Dreamacro/clash/component/dialer"
) )
// Adapter Type // Adapter Type
@ -27,6 +29,11 @@ const (
LoadBalance LoadBalance
) )
const (
DefaultTCPTimeout = 5 * time.Second
DefaultUDPTimeout = DefaultTCPTimeout
)
type Connection interface { type Connection interface {
Chains() Chain Chains() Chain
AppendToChains(adapter ProxyAdapter) AppendToChains(adapter ProxyAdapter)
@ -69,11 +76,14 @@ type PacketConn interface {
type ProxyAdapter interface { type ProxyAdapter interface {
Name() string Name() string
Type() AdapterType Type() AdapterType
Addr() string
SupportUDP() bool
MarshalJSON() ([]byte, error)
// StreamConn wraps a protocol around net.Conn with Metadata. // StreamConn wraps a protocol around net.Conn with Metadata.
// //
// Examples: // Examples:
// conn, _ := net.Dial("tcp", "host:port") // conn, _ := net.DialContext(context.Background(), "tcp", "host:port")
// conn, _ = adapter.StreamConn(conn, metadata) // conn, _ = adapter.StreamConn(conn, metadata)
// //
// It returns a C.Conn with protocol which start with // It returns a C.Conn with protocol which start with
@ -82,12 +92,10 @@ type ProxyAdapter interface {
// DialContext return a C.Conn with protocol which // DialContext return a C.Conn with protocol which
// contains multiplexing-related reuse logic (if any) // contains multiplexing-related reuse logic (if any)
DialContext(ctx context.Context, metadata *Metadata) (Conn, error) DialContext(ctx context.Context, metadata *Metadata, opts ...dialer.Option) (Conn, error)
ListenPacketContext(ctx context.Context, metadata *Metadata, opts ...dialer.Option) (PacketConn, error)
DialUDP(metadata *Metadata) (PacketConn, error)
SupportUDP() bool
MarshalJSON() ([]byte, error)
Addr() string
// Unwrap extracts the proxy from a proxy-group. It returns nil when nothing to extract. // Unwrap extracts the proxy from a proxy-group. It returns nil when nothing to extract.
Unwrap(metadata *Metadata) Proxy Unwrap(metadata *Metadata) Proxy
} }
@ -101,9 +109,14 @@ type Proxy interface {
ProxyAdapter ProxyAdapter
Alive() bool Alive() bool
DelayHistory() []DelayHistory DelayHistory() []DelayHistory
Dial(metadata *Metadata) (Conn, error)
LastDelay() uint16 LastDelay() uint16
URLTest(ctx context.Context, url string) (uint16, error) URLTest(ctx context.Context, url string) (uint16, error)
// Deprecated: use DialContext instead.
Dial(metadata *Metadata) (Conn, error)
// Deprecated: use DialPacketConn instead.
DialUDP(metadata *Metadata) (PacketConn, error)
} }
// AdapterType is enum of adapter type // AdapterType is enum of adapter type

70
constant/dns.go Normal file
View File

@ -0,0 +1,70 @@
package constant
import (
"encoding/json"
"errors"
)
// DNSModeMapping is a mapping for EnhancedMode enum
var DNSModeMapping = map[string]DNSMode{
DNSNormal.String(): DNSNormal,
DNSFakeIP.String(): DNSFakeIP,
DNSMapping.String(): DNSMapping,
}
const (
DNSNormal DNSMode = iota
DNSFakeIP
DNSMapping
)
type DNSMode int
// UnmarshalYAML unserialize EnhancedMode with yaml
func (e *DNSMode) UnmarshalYAML(unmarshal func(interface{}) error) error {
var tp string
if err := unmarshal(&tp); err != nil {
return err
}
mode, exist := DNSModeMapping[tp]
if !exist {
return errors.New("invalid mode")
}
*e = mode
return nil
}
// MarshalYAML serialize EnhancedMode with yaml
func (e DNSMode) MarshalYAML() (interface{}, error) {
return e.String(), nil
}
// UnmarshalJSON unserialize EnhancedMode with json
func (e *DNSMode) UnmarshalJSON(data []byte) error {
var tp string
json.Unmarshal(data, &tp)
mode, exist := DNSModeMapping[tp]
if !exist {
return errors.New("invalid mode")
}
*e = mode
return nil
}
// MarshalJSON serialize EnhancedMode with json
func (e DNSMode) MarshalJSON() ([]byte, error) {
return json.Marshal(e.String())
}
func (e DNSMode) String() string {
switch e {
case DNSNormal:
return "normal"
case DNSFakeIP:
return "fake-ip"
case DNSMapping:
return "redir-host"
default:
return "unknown"
}
}

7
constant/listener.go Normal file
View File

@ -0,0 +1,7 @@
package constant
type Listener interface {
RawAddress() string
Address() string
Close() error
}

View File

@ -17,7 +17,8 @@ const (
HTTP Type = iota HTTP Type = iota
HTTPCONNECT HTTPCONNECT
SOCKS SOCKS4
SOCKS5
REDIR REDIR
TPROXY TPROXY
) )
@ -43,7 +44,9 @@ func (t Type) String() string {
return "HTTP" return "HTTP"
case HTTPCONNECT: case HTTPCONNECT:
return "HTTP Connect" return "HTTP Connect"
case SOCKS: case SOCKS4:
return "Socks4"
case SOCKS5:
return "Socks5" return "Socks5"
case REDIR: case REDIR:
return "Redir" return "Redir"
@ -68,6 +71,7 @@ type Metadata struct {
DstPort string `json:"destinationPort"` DstPort string `json:"destinationPort"`
AddrType int `json:"-"` AddrType int `json:"-"`
Host string `json:"host"` Host string `json:"host"`
DNSMode DNSMode `json:"dnsMode"`
} }
func (m *Metadata) RemoteAddress() string { func (m *Metadata) RemoteAddress() string {
@ -82,14 +86,31 @@ func (m *Metadata) Resolved() bool {
return m.DstIP != nil return m.DstIP != nil
} }
// Pure is used to solve unexpected behavior
// when dialing proxy connection in DNSMapping mode.
func (m *Metadata) Pure() *Metadata {
if m.DNSMode == DNSMapping && m.DstIP != nil {
copy := *m
copy.Host = ""
if copy.DstIP.To4() != nil {
copy.AddrType = AtypIPv4
} else {
copy.AddrType = AtypIPv6
}
return &copy
}
return m
}
func (m *Metadata) UDPAddr() *net.UDPAddr { func (m *Metadata) UDPAddr() *net.UDPAddr {
if m.NetWork != UDP || m.DstIP == nil { if m.NetWork != UDP || m.DstIP == nil {
return nil return nil
} }
port, _ := strconv.Atoi(m.DstPort) port, _ := strconv.ParseInt(m.DstPort, 10, 16)
return &net.UDPAddr{ return &net.UDPAddr{
IP: m.DstIP, IP: m.DstIP,
Port: port, Port: int(port),
} }
} }

View File

@ -9,21 +9,19 @@ import (
const Name = "clash" const Name = "clash"
// Path is used to get the configuration path // Path is used to get the configuration path
var Path *path var Path = func() *path {
type path struct {
homeDir string
configFile string
}
func init() {
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {
homeDir, _ = os.Getwd() homeDir, _ = os.Getwd()
} }
homeDir = P.Join(homeDir, ".config", Name) homeDir = P.Join(homeDir, ".config", Name)
Path = &path{homeDir: homeDir, configFile: "config.yaml"} return &path{homeDir: homeDir, configFile: "config.yaml"}
}()
type path struct {
homeDir string
configFile string
} }
// SetHomeDir is used to set the configuration path // SetHomeDir is used to set the configuration path
@ -57,6 +55,10 @@ func (p *path) MMDB() string {
return P.Join(p.homeDir, "Country.mmdb") return P.Join(p.homeDir, "Country.mmdb")
} }
func (p *path) Cache() string { func (p *path) OldCache() string {
return P.Join(p.homeDir, ".cache") return P.Join(p.homeDir, ".cache")
} }
func (p *path) Cache() string {
return P.Join(p.homeDir, "cache.db")
}

View File

@ -0,0 +1,105 @@
package provider
import (
"github.com/Dreamacro/clash/constant"
)
// Vehicle Type
const (
File VehicleType = iota
HTTP
Compatible
)
// VehicleType defined
type VehicleType int
func (v VehicleType) String() string {
switch v {
case File:
return "File"
case HTTP:
return "HTTP"
case Compatible:
return "Compatible"
default:
return "Unknown"
}
}
type Vehicle interface {
Read() ([]byte, error)
Path() string
Type() VehicleType
}
// Provider Type
const (
Proxy ProviderType = iota
Rule
)
// ProviderType defined
type ProviderType int
func (pt ProviderType) String() string {
switch pt {
case Proxy:
return "Proxy"
case Rule:
return "Rule"
default:
return "Unknown"
}
}
// Provider interface
type Provider interface {
Name() string
VehicleType() VehicleType
Type() ProviderType
Initial() error
Update() error
}
// ProxyProvider interface
type ProxyProvider interface {
Provider
Proxies() []constant.Proxy
// ProxiesWithTouch is used to inform the provider that the proxy is actually being used while getting the list of proxies.
// Commonly used in DialContext and DialPacketConn
ProxiesWithTouch() []constant.Proxy
HealthCheck()
}
// Rule Type
const (
Domain RuleType = iota
IPCIDR
Classical
)
// RuleType defined
type RuleType int
func (rt RuleType) String() string {
switch rt {
case Domain:
return "Domain"
case IPCIDR:
return "IPCIDR"
case Classical:
return "Classical"
default:
return "Unknown"
}
}
// RuleProvider interface
type RuleProvider interface {
Provider
Behavior() RuleType
Match(*constant.Metadata) bool
ShouldResolveIP() bool
AsRule(adaptor string) constant.Rule
}

View File

@ -1,47 +0,0 @@
package context
import (
"net"
"net/http"
C "github.com/Dreamacro/clash/constant"
"github.com/gofrs/uuid"
)
type HTTPContext struct {
id uuid.UUID
metadata *C.Metadata
conn net.Conn
req *http.Request
}
func NewHTTPContext(conn net.Conn, req *http.Request, metadata *C.Metadata) *HTTPContext {
id, _ := uuid.NewV4()
return &HTTPContext{
id: id,
metadata: metadata,
conn: conn,
req: req,
}
}
// ID implement C.ConnContext ID
func (hc *HTTPContext) ID() uuid.UUID {
return hc.id
}
// Metadata implement C.ConnContext Metadata
func (hc *HTTPContext) Metadata() *C.Metadata {
return hc.metadata
}
// Conn implement C.ConnContext Conn
func (hc *HTTPContext) Conn() net.Conn {
return hc.conn
}
// Request return the http request struct
func (hc *HTTPContext) Request() *http.Request {
return hc.req
}

View File

@ -2,6 +2,7 @@ package dns
import ( import (
"context" "context"
"crypto/tls"
"fmt" "fmt"
"net" "net"
"strings" "strings"
@ -14,42 +15,46 @@ import (
type client struct { type client struct {
*D.Client *D.Client
r *Resolver r *Resolver
port string port string
host string host string
iface string
} }
func (c *client) Exchange(m *D.Msg) (msg *D.Msg, err error) { func (c *client) Exchange(m *D.Msg) (*D.Msg, error) {
return c.ExchangeContext(context.Background(), m) return c.ExchangeContext(context.Background(), m)
} }
func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) {
var ip net.IP var (
ip net.IP
err error
)
if c.r == nil { if c.r == nil {
// a default ip dns // a default ip dns
ip = net.ParseIP(c.host) if ip = net.ParseIP(c.host); ip == nil {
return nil, fmt.Errorf("dns %s not a valid ip", c.host)
}
} else { } else {
if ip, err = resolver.ResolveIPWithResolver(c.host, c.r); err != nil { if ip, err = resolver.ResolveIPWithResolver(c.host, c.r); err != nil {
return nil, fmt.Errorf("use default dns resolve failed: %w", err) return nil, fmt.Errorf("use default dns resolve failed: %w", err)
} }
} }
d, err := dialer.Dialer() network := "udp"
if strings.HasPrefix(c.Client.Net, "tcp") {
network = "tcp"
}
options := []dialer.Option{}
if c.iface != "" {
options = append(options, dialer.WithInterface(c.iface))
}
conn, err := dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), c.port), options...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer conn.Close()
if ip != nil && ip.IsGlobalUnicast() && dialer.DialHook != nil {
network := "udp"
if strings.HasPrefix(c.Client.Net, "tcp") {
network = "tcp"
}
if err := dialer.DialHook(d, network, ip); err != nil {
return nil, err
}
}
c.Client.Dialer = d
// miekg/dns ExchangeContext doesn't respond to context cancel. // miekg/dns ExchangeContext doesn't respond to context cancel.
// this is a workaround // this is a workaround
@ -59,7 +64,17 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err
} }
ch := make(chan result, 1) ch := make(chan result, 1)
go func() { go func() {
msg, _, err := c.Client.Exchange(m, net.JoinHostPort(ip.String(), c.port)) if strings.HasSuffix(c.Client.Net, "tls") {
conn = tls.Client(conn, c.Client.TLSConfig)
}
msg, _, err := c.Client.ExchangeWithConn(m, &D.Conn{
Conn: conn,
UDPSize: c.Client.UDPSize,
TsigSecret: c.Client.TsigSecret,
TsigProvider: c.Client.TsigProvider,
})
ch <- result{msg, err} ch <- result{msg, err}
}() }()

147
dns/dhcp.go Normal file
View File

@ -0,0 +1,147 @@
package dns
import (
"bytes"
"context"
"net"
"sync"
"time"
"github.com/Dreamacro/clash/component/dhcp"
"github.com/Dreamacro/clash/component/iface"
"github.com/Dreamacro/clash/component/resolver"
D "github.com/miekg/dns"
)
const (
IfaceTTL = time.Second * 20
DHCPTTL = time.Hour
DHCPTimeout = time.Minute
)
type dhcpClient struct {
ifaceName string
lock sync.Mutex
ifaceInvalidate time.Time
dnsInvalidate time.Time
ifaceAddr *net.IPNet
done chan struct{}
resolver *Resolver
err error
}
func (d *dhcpClient) Exchange(m *D.Msg) (msg *D.Msg, err error) {
ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout)
defer cancel()
return d.ExchangeContext(ctx, m)
}
func (d *dhcpClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
res, err := d.resolve(ctx)
if err != nil {
return nil, err
}
return res.ExchangeContext(ctx, m)
}
func (d *dhcpClient) resolve(ctx context.Context) (*Resolver, error) {
d.lock.Lock()
invalidated, err := d.invalidate()
if err != nil {
d.err = err
} else if invalidated {
done := make(chan struct{})
d.done = done
go func() {
ctx, cancel := context.WithTimeout(context.Background(), DHCPTimeout)
defer cancel()
var res *Resolver
dns, err := dhcp.ResolveDNSFromDHCP(ctx, d.ifaceName)
if err == nil {
nameserver := make([]NameServer, 0, len(dns))
for _, item := range dns {
nameserver = append(nameserver, NameServer{
Addr: net.JoinHostPort(item.String(), "53"),
Interface: d.ifaceName,
})
}
res = NewResolver(Config{
Main: nameserver,
})
}
d.lock.Lock()
defer d.lock.Unlock()
close(done)
d.done = nil
d.resolver = res
d.err = err
}()
}
d.lock.Unlock()
for {
d.lock.Lock()
res, err, done := d.resolver, d.err, d.done
d.lock.Unlock()
// initializing
if res == nil && err == nil {
select {
case <-done:
continue
case <-ctx.Done():
return nil, ctx.Err()
}
}
// dirty return
return res, err
}
}
func (d *dhcpClient) invalidate() (bool, error) {
if time.Now().Before(d.ifaceInvalidate) {
return false, nil
}
d.ifaceInvalidate = time.Now().Add(IfaceTTL)
ifaceObj, err := iface.ResolveInterface(d.ifaceName)
if err != nil {
return false, err
}
addr, err := ifaceObj.PickIPv4Addr(nil)
if err != nil {
return false, err
}
if time.Now().Before(d.dnsInvalidate) && d.ifaceAddr.IP.Equal(addr.IP) && bytes.Equal(d.ifaceAddr.Mask, addr.Mask) {
return false, nil
}
d.dnsInvalidate = time.Now().Add(DHCPTTL)
d.ifaceAddr = addr
return d.done == nil, nil
}
func newDHCPClient(ifaceName string) *dhcpClient {
return &dhcpClient{ifaceName: ifaceName}
}

View File

@ -3,8 +3,7 @@ package dns
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/tls" "io"
"io/ioutil"
"net" "net"
"net/http" "net/http"
@ -29,13 +28,21 @@ func (dc *dohClient) Exchange(m *D.Msg) (msg *D.Msg, err error) {
} }
func (dc *dohClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { func (dc *dohClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
req, err := dc.newRequest(m) // https://datatracker.ietf.org/doc/html/rfc8484#section-4.1
// In order to maximize cache friendliness, SHOULD use a DNS ID of 0 in every DNS request.
newM := *m
newM.Id = 0
req, err := dc.newRequest(&newM)
if err != nil { if err != nil {
return nil, err return nil, err
} }
req = req.WithContext(ctx) req = req.WithContext(ctx)
return dc.doRequest(req) msg, err = dc.doRequest(req)
if err == nil {
msg.Id = m.Id
}
return
} }
// newRequest returns a new DoH request given a dns.Msg. // newRequest returns a new DoH request given a dns.Msg.
@ -63,7 +70,7 @@ func (dc *dohClient) doRequest(req *http.Request) (msg *D.Msg, err error) {
} }
defer resp.Body.Close() defer resp.Body.Close()
buf, err := ioutil.ReadAll(resp.Body) buf, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -76,7 +83,6 @@ func newDoHClient(url string, r *Resolver) *dohClient {
return &dohClient{ return &dohClient{
url: url, url: url,
transport: &http.Transport{ transport: &http.Transport{
TLSClientConfig: &tls.Config{ClientSessionCache: globalSessionCache},
ForceAttemptHTTP2: true, ForceAttemptHTTP2: true,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
host, port, err := net.SplitHostPort(addr) host, port, err := net.SplitHostPort(addr)

View File

@ -5,20 +5,21 @@ import (
"github.com/Dreamacro/clash/common/cache" "github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/component/fakeip" "github.com/Dreamacro/clash/component/fakeip"
C "github.com/Dreamacro/clash/constant"
) )
type ResolverEnhancer struct { type ResolverEnhancer struct {
mode EnhancedMode mode C.DNSMode
fakePool *fakeip.Pool fakePool *fakeip.Pool
mapping *cache.LruCache mapping *cache.LruCache
} }
func (h *ResolverEnhancer) FakeIPEnabled() bool { func (h *ResolverEnhancer) FakeIPEnabled() bool {
return h.mode == FAKEIP return h.mode == C.DNSFakeIP
} }
func (h *ResolverEnhancer) MappingEnabled() bool { func (h *ResolverEnhancer) MappingEnabled() bool {
return h.mode == FAKEIP || h.mode == MAPPING return h.mode == C.DNSFakeIP || h.mode == C.DNSMapping
} }
func (h *ResolverEnhancer) IsExistFakeIP(ip net.IP) bool { func (h *ResolverEnhancer) IsExistFakeIP(ip net.IP) bool {
@ -67,7 +68,7 @@ func (h *ResolverEnhancer) PatchFrom(o *ResolverEnhancer) {
} }
if h.fakePool != nil && o.fakePool != nil { if h.fakePool != nil && o.fakePool != nil {
h.fakePool.PatchFrom(o.fakePool) h.fakePool.CloneFrom(o.fakePool)
} }
} }
@ -75,7 +76,7 @@ func NewEnhancer(cfg Config) *ResolverEnhancer {
var fakePool *fakeip.Pool var fakePool *fakeip.Pool
var mapping *cache.LruCache var mapping *cache.LruCache
if cfg.EnhancedMode != NORMAL { if cfg.EnhancedMode != C.DNSNormal {
fakePool = cfg.Pool fakePool = cfg.Pool
mapping = cache.NewLRUCache(cache.WithSize(4096), cache.WithStale(true)) mapping = cache.NewLRUCache(cache.WithSize(4096), cache.WithStale(true))
} }

View File

@ -2,6 +2,7 @@ package dns
import ( import (
"net" "net"
"strings"
"github.com/Dreamacro/clash/component/mmdb" "github.com/Dreamacro/clash/component/mmdb"
"github.com/Dreamacro/clash/component/trie" "github.com/Dreamacro/clash/component/trie"
@ -11,11 +12,13 @@ type fallbackIPFilter interface {
Match(net.IP) bool Match(net.IP) bool
} }
type geoipFilter struct{} type geoipFilter struct {
code string
}
func (gf *geoipFilter) Match(ip net.IP) bool { func (gf *geoipFilter) Match(ip net.IP) bool {
record, _ := mmdb.Instance().Country(ip) record, _ := mmdb.Instance().Country(ip)
return record.Country.IsoCode != "CN" && record.Country.IsoCode != "" return !strings.EqualFold(record.Country.IsoCode, gf.code) && !ip.IsPrivate()
} }
type ipnetFilter struct { type ipnetFilter struct {
@ -29,6 +32,7 @@ func (inf *ipnetFilter) Match(ip net.IP) bool {
type fallbackDomainFilter interface { type fallbackDomainFilter interface {
Match(domain string) bool Match(domain string) bool
} }
type domainFilter struct { type domainFilter struct {
tree *trie.DomainTrie tree *trie.DomainTrie
} }

Some files were not shown because too many files have changed in this diff Show More