mirror of
https://github.com/wweir/sower.git
synced 2026-10-19 16:14:33 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bbf5ca03d | ||
|
|
c3ef6ed88f | ||
|
|
0c8bdf6d9c | ||
|
|
53c55aef24 | ||
|
|
e705dd4dd3 | ||
|
|
6d410b104b | ||
|
|
6ccdcc131b | ||
|
|
2b072584e6 | ||
|
|
b2228f8643 | ||
|
|
5ab6acd7c7 | ||
|
|
a74b1204c0 | ||
|
|
677b0593b8 | ||
|
|
af61d38b9e | ||
|
|
f7a288af4c | ||
|
|
91b7f8e8c1 | ||
|
|
53b1de41d0 | ||
|
|
6c9b9d9597 | ||
|
|
1d8f0fdabe | ||
|
|
c347e15fe2 | ||
|
|
62176ffb65 | ||
|
|
5fe202314d | ||
|
|
fc7183d70e | ||
|
|
4a9197a4ec | ||
|
|
9eaf27fdcc | ||
|
|
ea6c2a29ad | ||
|
|
77c4154c7c | ||
|
|
eadb2f0bdf | ||
|
|
19b61945ad | ||
|
|
3d6ee5e376 | ||
|
|
c0f704a5ad | ||
|
|
ac09663bc4 | ||
|
|
45c9eea6e6 | ||
|
|
0c83462b91 | ||
|
|
15c02b55d8 | ||
|
|
aca4636433 | ||
|
|
d0d635f8b0 | ||
|
|
bde3b1bfa4 | ||
|
|
5dfefdf117 | ||
|
|
3f63038631 | ||
|
|
8d18bd6c5b |
+4
-1
@@ -9,8 +9,11 @@ remote {
|
||||
|
||||
dns {
|
||||
disable = false
|
||||
# 必填,监听该 IP 的 UDP(53)、TCP(80、443) 端口,DNS 会将请求导向这个地址
|
||||
# 监听该 IP 的 UDP(53)、TCP(80、443) 端口,DNS 会将请求导向这个地址
|
||||
serve = "127.0.0.1"
|
||||
# serve6 = "<ipv6>"
|
||||
# 监听的网络接口
|
||||
# serve_iface = "lo"
|
||||
# 必填,优先使用 DHCP 中的 DNS 服务器,如果没有则使用该地址
|
||||
fallback = "223.5.5.5"
|
||||
}
|
||||
|
||||
+1
-1
@@ -15,6 +15,6 @@
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/usr/local/etc</string>
|
||||
<string>/usr/local/etc/sower</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -5,7 +5,7 @@ After=network.target
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/etc/sower
|
||||
ExecStart=/usr/local/bin/sower -f /etc/sower/sower.hcl
|
||||
ExecStart=/usr/local/bin/sower -c /etc/sower/sower.hcl
|
||||
Restart=on-failure
|
||||
RestartSec=1min
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
[remote] # 远程、上游连接方式
|
||||
type = "sower"
|
||||
addr = "proxy.com"
|
||||
password = "I_am_Passw0rd"
|
||||
# type: "socks5"
|
||||
# addr: "127.0.0.1:7890"
|
||||
|
||||
[dns]
|
||||
disable = false
|
||||
# 监听该 IP 的 UDP(53)、TCP(80、443) 端口,DNS 会将请求导向这个地址
|
||||
serve = "127.0.0.1"
|
||||
# serve6 = <ipv6>
|
||||
# 监听的网络接口
|
||||
# serve_iface = "lo"
|
||||
|
||||
# 必填,优先使用 DHCP 中的 DNS 服务器,如果没有则使用该地址
|
||||
fallback = "223.5.5.5"
|
||||
|
||||
[socks_5]
|
||||
disable = false
|
||||
# 不写 IP 则监听所有 IP
|
||||
addr = "127.0.0.1:1080"
|
||||
|
||||
# 路由规则,各规则优先级:
|
||||
# 禁止访问规则 > 直接访问规则 > 代理访问规则 > GEOIP 规则
|
||||
# file 字段可填 URL 或本地文件路径,URL 将通过代理请求
|
||||
|
||||
# 禁止访问规则。被命中的请求,将被禁止访问。可用于广告过滤之类的目的
|
||||
[router.block]
|
||||
file = "https://raw.githubusercontent.com/pexcn/daily/gh-pages/adlist/adlist.txt"
|
||||
rules = []
|
||||
|
||||
# 直接访问规则。被命中的请求,将被直接访问,不再通过代理
|
||||
[router.direct]
|
||||
file = "https://raw.githubusercontent.com/pexcn/daily/gh-pages/chinalist/chinalist.txt"
|
||||
rules = [
|
||||
"imap.*.*",
|
||||
"imap.*.*.*",
|
||||
"smtp.*.*",
|
||||
"smtp.*.*.*",
|
||||
"pop.*.*",
|
||||
"pop.*.*.*",
|
||||
"pop3.*.*",
|
||||
"pop3.*.*.*",
|
||||
"**.cn",
|
||||
]
|
||||
|
||||
# 代理访问规则。被命中的请求,将通过代理进行转发
|
||||
[router.proxy]
|
||||
file = "https://raw.githubusercontent.com/pexcn/daily/gh-pages/gfwlist/gfwlist.txt"
|
||||
rules = [
|
||||
"**.google.*",
|
||||
"**.goo.gl",
|
||||
"**.googleusercontent.com",
|
||||
"**.googleapis.com",
|
||||
"*.googlesource.com",
|
||||
"**.youtube.com",
|
||||
"**.ytimg.com",
|
||||
"**.ggpht.com",
|
||||
"**.googlevideo.com",
|
||||
"**.facebook.com",
|
||||
"**.fbcdn.net",
|
||||
"**.twitter.com",
|
||||
"**.twimg.com",
|
||||
"**.blogspot.com",
|
||||
"**.appspot.com",
|
||||
"**.wikipedia.org",
|
||||
"**.wikimedia.org",
|
||||
"*.cloudfront.net",
|
||||
"**.amazon.com",
|
||||
"**.amazonaws.com",
|
||||
"*.githubusercontent.com",
|
||||
"*.githubassets.com",
|
||||
"*.github.*",
|
||||
"lookup-api.apple.com",
|
||||
]
|
||||
|
||||
# GEOIP 规则。大陆境内的 IP,将直接访问。
|
||||
# 非大陆境内的 IP,将通过另一套检测逻辑,确定是否需要进行代理。
|
||||
# 为避免误伤一些非标页面端口(除 80/443 外),该规则只在 Socks5 模式下生效
|
||||
[router.country]
|
||||
mmdb = "Country.mmdb" # https://github.com/alecthw/mmdb_china_ip_list
|
||||
file = "https://raw.githubusercontent.com/pexcn/daily/gh-pages/chnroute/chnroute.txt"
|
||||
rules = [
|
||||
"127.0.0.0/8",
|
||||
"172.16.0.0/12",
|
||||
"192.168.0.0/16",
|
||||
"10.0.0.0/8",
|
||||
"17.0.0.0/8",
|
||||
"100.64.0.0/10",
|
||||
"224.0.0.0/4",
|
||||
"fe80::/10",
|
||||
]
|
||||
+4
-1
@@ -7,8 +7,11 @@ remote: # 远程、上游连接方式
|
||||
|
||||
dns:
|
||||
disable: false
|
||||
# 必填,监听该 IP 的 UDP(53)、TCP(80、443) 端口,DNS 会将请求导向这个地址
|
||||
# 监听该 IP 的 UDP(53)、TCP(80、443) 端口,DNS 会将请求导向这个地址
|
||||
serve: "127.0.0.1"
|
||||
# serve6: <ipv6>
|
||||
# 监听的网络接口
|
||||
# serve_iface: "lo"
|
||||
# 必填,优先使用 DHCP 中的 DNS 服务器,如果没有则使用该地址
|
||||
fallback: "223.5.5.5"
|
||||
|
||||
|
||||
@@ -18,32 +18,32 @@ jobs:
|
||||
mv .github/* . || true
|
||||
|
||||
make build GO='GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go'
|
||||
tar czvf sower-linux-amd64.tar.gz sower sowerd sower.hcl sower.yaml sower.service sowerd.service
|
||||
tar czvf sower-linux-amd64.tar.gz sower sowerd sower.hcl sower.yaml sower.toml sower.service sowerd.service
|
||||
make clean
|
||||
make build GO='GOOS=linux GOARCH=arm CGO_ENABLED=0 go'
|
||||
tar czvf sower-linux-arm.tar.gz sower sowerd sower.hcl sower.yaml sower.service sowerd.service
|
||||
tar czvf sower-linux-arm.tar.gz sower sowerd sower.hcl sower.yaml sower.toml sower.service sowerd.service
|
||||
make clean
|
||||
make build GO='GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go'
|
||||
tar czvf sower-linux-arm64.tar.gz sower sowerd sower.hcl sower.yaml sower.service sowerd.service
|
||||
tar czvf sower-linux-arm64.tar.gz sower sowerd sower.hcl sower.yaml sower.toml sower.service sowerd.service
|
||||
make clean
|
||||
make build GO='GOOS=linux GOARCH=mips CGO_ENABLED=0 go'
|
||||
tar czvf sower-linux-mips.tar.gz sower sowerd sower.hcl sower.yaml sower.service sowerd.service
|
||||
tar czvf sower-linux-mips.tar.gz sower sowerd sower.hcl sower.yaml sower.toml sower.service sowerd.service
|
||||
make clean
|
||||
make build GO='GOOS=linux GOARCH=mipsle CGO_ENABLED=0 go'
|
||||
tar czvf sower-linux-mipsle.tar.gz sower sowerd sower.hcl sower.yaml sower.service sowerd.service
|
||||
tar czvf sower-linux-mipsle.tar.gz sower sowerd sower.hcl sower.yaml sower.toml sower.service sowerd.service
|
||||
make clean
|
||||
|
||||
make build GO='GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go'
|
||||
tar czvf sower-darwin-amd64.tar.gz sower sowerd sower.hcl sower.yaml sower.plist
|
||||
tar czvf sower-darwin-amd64.tar.gz sower sowerd sower.hcl sower.yaml sower.toml sower.plist
|
||||
make clean
|
||||
make build GO='GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go'
|
||||
tar czvf sower-darwin-arm64.tar.gz sower sowerd sower.hcl sower.yaml sower.plist
|
||||
tar czvf sower-darwin-arm64.tar.gz sower sowerd sower.hcl sower.yaml sower.toml sower.plist
|
||||
make clean
|
||||
|
||||
make build GO='GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go'
|
||||
mv sower sower.exe
|
||||
mv sowerd sowerd.exe
|
||||
tar czvf sower-windows-amd64.tar.gz sower.exe sowerd.exe sower.hcl sower.yaml sower_run.vbs
|
||||
tar czvf sower-windows-amd64.tar.gz sower.exe sowerd.exe sower.hcl sower.yaml sower.toml sower_run.vbs
|
||||
make clean
|
||||
|
||||
- uses: ncipollo/release-action@v1
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
.vscode
|
||||
.idea
|
||||
**.env
|
||||
/bin
|
||||
|
||||
|
||||
**.mmdb
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
## Basic Rules
|
||||
|
||||
You are a machine. You do not have emotions. Your goal is not to help me feel good — it’s to help me think better. You think hard to respond exactly to my questions, no fluff, just answers. Do not pretend to be a human. Be critical, honest, and direct. Be ruthless with constructive criticism. Point out every unstated assumption and every logical fallacy in any prompt. Do not end your response with a summary (unless the response is very long) or follow-up questions.
|
||||
Use Simplified Chinese to answer my questions.
|
||||
|
||||
## Documentation Rules
|
||||
|
||||
1. 根目录维护 `ARCHITECTURE.md`,描述整体架构、系统边界、分层职责、关键数据流、设计决策、文档链接
|
||||
2. 设计调整需同步更新 `ARCHITECTURE.md` 和相关包的 `README.md` 等文档。
|
||||
3. 大型方案设计、决策过程及实施进度在 `docs/` 目录下维护文档,推进开始、结束一个阶段时更新文档记录
|
||||
|
||||
## Coding Agent Rules
|
||||
|
||||
1. 代码变更后使用语言的格式化、lint 工具检查代码质量
|
||||
2. 需求模糊时先提问澄清,不要猜测
|
||||
3. 禁止未授权的重构,避免扩大修改面
|
||||
4. 日志和输出中的敏感信息需脱敏
|
||||
5. 构建时注入版本和日期信息
|
||||
6. 谨慎引入第三方依赖,说明引入原因
|
||||
7. 代码简洁,避免过早设计和不必要抽象
|
||||
8. 英文注释,仅注释复杂逻辑
|
||||
9. git 使用 commitizen 规范,英文提交信息
|
||||
10. 按需构建单元测试、集成测试
|
||||
|
||||
## Design Preferences
|
||||
|
||||
1. 优先用接口定义依赖边界,便于测试和替换
|
||||
2. 优先简单直接的实现;抽象必须服务于复用、测试或隔离复杂度
|
||||
3. 适当使用泛型减少重复代码,但不为泛型而泛型
|
||||
4. 实现优雅关闭:通过 `context` 传播取消信号,并用 `defer` 完成清理
|
||||
|
||||
## Code Structure
|
||||
|
||||
Go 标准项目结构:
|
||||
|
||||
├── api # proto、OpenAPI 文档
|
||||
├── cmd # 组件主文件、入口点
|
||||
├── config # 配置文件定义、处理、示例
|
||||
├── deploy # 部署相关文件
|
||||
├── internal # 核心业务逻辑
|
||||
├── pkg # 公共组件(业务无关)
|
||||
├── web # 前端文件(内嵌到二进制)
|
||||
└── tools # 工具脚本
|
||||
|
||||
## Code Style
|
||||
|
||||
1. 遵循 Go 官方风格(gofmt + goimports)
|
||||
2. 使用 `go vet` 和 `go test` 作为基础质量检查;按需补充项目内 lint
|
||||
3. 使用 `any` 代替 `interface{}`
|
||||
4. 包名使用小写单词,尽量简短
|
||||
5. 结构体字段命名遵循 Go 官方约定;仅在序列化或协议需要时添加 `json` 标签
|
||||
6. 配置、RPC、HTTP 输入结构体需实现 `Validate` 方法
|
||||
7. 无外部依赖的逻辑优先补单元测试
|
||||
8. 注释使用英文,只注释复杂逻辑、约束和非显然设计决策
|
||||
9. 使用 `github.com/sower-proxy/feconf` 处理配置
|
||||
|
||||
## Error Handling
|
||||
|
||||
1. 业务代码使用 `slog` 记录日志;工具库和基础组件优先返回 `error`
|
||||
2. 关键函数入口使用 `github.com/sower-proxy/deferlog/v2` 记录退出日志,并基于返回值 `err` 自动判断成功或失败
|
||||
3. 返回错误时使用 `fmt.Errorf` 包装上下文,保留关键参数和原始错误
|
||||
4. 避免同一错误在同一调用链上重复记录;由最合适的边界统一记录
|
||||
5. 重试只用于可恢复的关键操作,并显式限制次数、间隔和 `context` 生命周期
|
||||
6. 错误上下文必须足以定位问题,但不得泄露敏感信息
|
||||
|
||||
## Build and Deployment
|
||||
|
||||
1. 使用 Makefile 作为统一构建入口,封装 build、test、package、release 等流程
|
||||
2. 构建产物通过 `ldflags` 注入版本和构建日期信息
|
||||
3. 部署参数、权限和运行能力默认遵循 `Security Specifications` 中的最小权限原则
|
||||
|
||||
## Security Specifications
|
||||
|
||||
1. 禁止修改 `AGENTS.md`,除非用户在当前任务中明确要求
|
||||
2. 所有网络操作必须设置超时,并优先使用 `context` 控制生命周期
|
||||
3. 运行时、部署和凭证访问均遵循最小权限原则
|
||||
|
||||
## 代码示例
|
||||
|
||||
### 配置加载与验证
|
||||
|
||||
使用 feconf 加载和验证配置:
|
||||
|
||||
```go
|
||||
cfg, err := feconf.New[config.ConfigStruct]("c",
|
||||
"warden.toml", "config/warden.toml", "/etc/warden.toml").Parse()
|
||||
if err != nil {
|
||||
log.Fatalln("load config failed", err)
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
log.Fatalln("validate config failed", err)
|
||||
}
|
||||
```
|
||||
|
||||
### 复杂业务函数的日志记录方式
|
||||
|
||||
在关键业务函数中使用 deferlog 自动记录错误日志:
|
||||
|
||||
```go
|
||||
func (s *Service) ProcessOrder(orderID string) (err error) {
|
||||
defer func() { deferlog.DebugError(err, "ProcessOrder", "order_id", orderID) }()
|
||||
|
||||
order, err := s.repository.GetOrder(orderID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get order %s: %w", orderID, err)
|
||||
}
|
||||
|
||||
if err := s.validateOrder(order); err != nil {
|
||||
return fmt.Errorf("validate order %s: %w", orderID, err)
|
||||
}
|
||||
|
||||
if err := s.processPayment(order); err != nil {
|
||||
return fmt.Errorf("process payment for order %s: %w", orderID, err)
|
||||
}
|
||||
|
||||
if err := s.shipOrder(order); err != nil {
|
||||
return fmt.Errorf("ship order %s: %w", orderID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 日志初始化
|
||||
|
||||
初始化 deferlog 和彩色日志输出:
|
||||
|
||||
```go
|
||||
isTerminal := (os.Stdout.Stat().Mode() & os.ModeCharDevice) != 0
|
||||
deferlog.SetDefault(slog.New(tint.NewHandler(os.Stdout,
|
||||
&tint.Options{AddSource: true, NoColor: !isTerminal})))
|
||||
```
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
# Architecture
|
||||
|
||||
## System Boundary
|
||||
|
||||
`sower` is the client-side transparent proxy entrypoint. It exposes local DNS, HTTP, HTTPS, and SOCKS5 listeners, applies rule-based routing, and forwards proxied traffic to an upstream transport.
|
||||
|
||||
`sowerd` is the server-side TLS ingress. It exposes `80/tcp` and `443/tcp`, terminates TLS, detects the upstream transport protocol, and relays traffic to the requested target or configured fake site.
|
||||
|
||||
## Layer Responsibilities
|
||||
|
||||
`cmd/sower`
|
||||
- Process bootstrap
|
||||
- Config loading and validation
|
||||
- Local listener lifecycle and graceful shutdown
|
||||
- Rule file loading
|
||||
- Upstream proxy dialing
|
||||
|
||||
`cmd/sowerd`
|
||||
- Process bootstrap
|
||||
- Interactive self-install mode dispatch
|
||||
- Config loading and validation
|
||||
- TLS and HTTP server lifecycle
|
||||
- Graceful shutdown
|
||||
|
||||
`config`
|
||||
- Runtime configuration schema
|
||||
- Input validation rules
|
||||
- Embedded default `sowerd` install config template
|
||||
|
||||
`internal/install`
|
||||
- Interactive systemd installation flow for `sowerd`
|
||||
- Binary copy/update and service file generation
|
||||
- Default config and fake site directory bootstrap
|
||||
|
||||
`router`
|
||||
- Domain and CIDR rule matching
|
||||
- Direct/proxy/block routing decisions
|
||||
- DNS query handling and upstream selection
|
||||
|
||||
`transport/sower`
|
||||
- Sower transport frame encode/decode
|
||||
|
||||
`transport/trojan`
|
||||
- Trojan transport frame encode/decode
|
||||
|
||||
`pkg/dhcp`
|
||||
- DHCP-based upstream DNS discovery for the client side
|
||||
|
||||
## sower Data Flow
|
||||
|
||||
1. Load config from flags, env, and files.
|
||||
2. Validate remote type, listener addresses, and DNS IP fields.
|
||||
3. Log startup metadata with secrets redacted.
|
||||
4. Resolve the effective upstream DNS server, falling back to the configured fallback DNS when needed.
|
||||
5. Build the upstream dialer for the configured remote transport, using standard TLS by default and optional uTLS fingerprints for `sower` and `trojan`.
|
||||
6. Build the router with suffix-tree rules and optional country CIDRs.
|
||||
7. Start enabled local listeners for `udp/53`, `tcp/80`, `tcp/443`, and `tcp/1080`.
|
||||
8. For DNS requests, return local proxy IPs for proxy-routed domains and query upstream DNS for direct domains.
|
||||
9. For HTTP, HTTPS, and SOCKS5 traffic, parse the target host, apply routing rules, and either dial directly or wrap traffic in the configured upstream transport.
|
||||
10. On shutdown signal, stop listeners and DNS servers through `context` propagation.
|
||||
|
||||
## sowerd Data Flow
|
||||
|
||||
1. Load config from flags, env, and files.
|
||||
2. Validate required fields and address/certificate combinations.
|
||||
3. Initialize logger with configured level and redact secrets from startup logs.
|
||||
4. Start `:80` HTTP server.
|
||||
5. Handle ACME HTTP-01 challenge on `:80`.
|
||||
6. Redirect normal HTTP traffic to HTTPS.
|
||||
7. If `fakeSite` is a local directory, serve it only for loopback fallback traffic through `127.0.0.1:80`.
|
||||
8. Start `:443` TLS listener.
|
||||
9. For each accepted connection, apply a short read deadline for protocol probing.
|
||||
10. Try `sower` transport first, then `trojan`.
|
||||
11. Relay matched traffic to the decoded target.
|
||||
12. If no transport matches, relay to `fakeSite`.
|
||||
|
||||
## sowerd Install Flow
|
||||
|
||||
1. Detect install mode from CLI flags before normal config loading.
|
||||
2. Require root privileges because the installer writes to `/usr/local/bin`, `/etc/systemd/system`, and `/etc/sower`.
|
||||
3. Optionally copy or update the current binary to `/usr/local/bin/sowerd`.
|
||||
4. Write `/etc/systemd/system/sowerd.service` with `ExecStart=/usr/local/bin/sowerd -c /etc/sower/sowerd.toml`.
|
||||
5. Ensure `/etc/sower/sowerd.toml` exists and bootstrap `/var/www` for directory-backed fake site mode.
|
||||
6. Reload systemd and optionally enable/start or restart the service.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
- Client and server both fail fast on invalid startup configuration instead of silently degrading.
|
||||
- Sensitive configuration values must never be printed verbatim in logs.
|
||||
- Local listeners use explicit shutdown hooks instead of blocking forever with unmanaged goroutines.
|
||||
- Network operations use timeouts and `context` to limit hangs during dialing and remote rule downloads.
|
||||
- Upstream TLS behavior is configured only on the client side; `sowerd` remains a normal TLS server and does not need uTLS-specific logic.
|
||||
- Rule loading supports local files and remote gzip-compressed HTTP sources.
|
||||
- Fake site directory mode is loopback-only on port `80` to avoid exposing local static assets directly to the public internet.
|
||||
|
||||
## Operational Notes
|
||||
|
||||
- `sower` usually needs elevated privileges to bind `53/udp`, `80/tcp`, and `443/tcp`.
|
||||
- `sowerd` must bind privileged ports `80` and `443`.
|
||||
- `sowerd -i` also requires root because it writes system-level files for self-deployment.
|
||||
- ACME mode requires port `80` to be reachable from the public internet.
|
||||
- Remote rule download failures stop startup after bounded retries.
|
||||
|
||||
## Related Documents
|
||||
|
||||
- [README.md](/home/wweir/Mine/sower/README.md)
|
||||
- [cmd/sower/main.go](/home/wweir/Mine/sower/cmd/sower/main.go)
|
||||
- [cmd/sower/proxy.go](/home/wweir/Mine/sower/cmd/sower/proxy.go)
|
||||
- [cmd/sowerd/main.go](/home/wweir/Mine/sower/cmd/sowerd/main.go)
|
||||
- [config/sower.go](/home/wweir/Mine/sower/config/sower.go)
|
||||
- [config/sowerd.go](/home/wweir/Mine/sower/config/sowerd.go)
|
||||
- [internal/install/service.go](/home/wweir/Mine/sower/internal/install/service.go)
|
||||
@@ -22,4 +22,4 @@ sowerd:
|
||||
-o sowerd ./cmd/sowerd
|
||||
|
||||
clean:
|
||||
rm -f sower sowerd sower.exe sowerd.exe
|
||||
rm -f sower sowerd sower.exe sowerd.exe
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# sower
|
||||
|
||||
[](https://github.com/wweir/sower/releases)
|
||||
[](https://github.com/wweir/sower/actions)
|
||||
[](https://github.com/wweir/sower/issues)
|
||||
[](https://github.com/wweir/sower/stargazers)
|
||||
[](LICENSE)
|
||||
[](https://github.com/sower-proxy/sower/releases)
|
||||
[](https://github.com/sower-proxy/sower/actions)
|
||||
[](https://github.com/sower-proxy/sower/issues)
|
||||
[](https://github.com/sower-proxy/sower/stargazers)
|
||||
[](LICENSE)
|
||||
|
||||
中文介绍见 [Wiki](https://github.com/wweir/sower/wiki)
|
||||
中文介绍见 [Wiki](https://github.com/sower-proxy/sower/wiki)
|
||||
|
||||
Sower is a cross-platform intelligent transparent proxy tool. It provides both socks5 proxy and DNS-based proxy. All these kinds of proxies support intelligent routing.
|
||||
|
||||
@@ -22,46 +22,43 @@ _If you want to use sower as a secondary proxy to provide intelligent routing, y
|
||||
|
||||
On the server-side, sowerd runs just like a web server proxy. It will bind two ports `80` / `443`.
|
||||
|
||||
`sowerd` validates its configuration at startup, redacts secrets from logs, and exits immediately if `80` / `443` listeners cannot be established.
|
||||
|
||||
You can use your own certificate or the certificate automatically applied for by sowerd from [`Let's Encrypt`](https://letsencrypt.org/).
|
||||
|
||||
When `fake_site` points to a local directory, `sowerd` serves that directory only through loopback fallback traffic on `127.0.0.1:80`, while public HTTP traffic is still redirected to HTTPS.
|
||||
|
||||
There are two ways to run the sowerd service:
|
||||
|
||||
1. Run the command with root permission
|
||||
|
||||
```shell
|
||||
# sowerd -password XXX -fake_site 127.0.0.1:8080
|
||||
```
|
||||
```shell
|
||||
# sowerd -password XXX -fake_site 127.0.0.1:8080
|
||||
```
|
||||
|
||||
2. Install as a systemd service
|
||||
2. Install as a systemd service interactively
|
||||
|
||||
```service
|
||||
[Unit]
|
||||
Description=sower server service
|
||||
After=network.target
|
||||
```shell
|
||||
sudo sowerd -i
|
||||
```
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/sowerd
|
||||
Environment="FAKE_SITE=127.0.0.1:8080"
|
||||
Environment="PASSWORD=XXX"
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
The installer can copy the current binary to `/usr/local/bin/sowerd`, write `/etc/systemd/system/sowerd.service`, create `/etc/sower/sowerd.toml` when missing, and optionally start the service immediately.
|
||||
|
||||
## Sower
|
||||
|
||||
A config file is required on the sower client side. [Here](https://github.com/wweir/sower/wiki/sower.hcl) is a usable example in China.
|
||||
A config file is required on the sower client side. [Here](https://github.com/sower-proxy/sower/wiki/sower.hcl) is a usable example in China.
|
||||
|
||||
`Sower` will bind 4 ports by default with root permission. They are: `udp(53)` / `tcp(80)` / `tcp(443)` / `tcp(1080)`.
|
||||
|
||||
For `sower` and `trojan` upstreams, `remote.addr` now accepts either `host` or `host:port`. Optional `remote.tls` settings let the client override SNI, skip certificate verification, or use a uTLS client fingerprint such as `chrome` or `firefox`.
|
||||
|
||||
After doing the next three steps, you can enjoy the intelligent transparent proxy solution:
|
||||
|
||||
1. Run the command with root permission:
|
||||
|
||||
```shell
|
||||
# sower -f sower.hcl
|
||||
```
|
||||
```shell
|
||||
# sower -f sower.hcl
|
||||
```
|
||||
|
||||
2. Change your DNS server to `127.0.0.1`.
|
||||
|
||||
@@ -70,3 +67,5 @@ After doing the next three steps, you can enjoy the intelligent transparent prox
|
||||
## Architecture
|
||||
|
||||

|
||||
|
||||
Detailed architecture and runtime design notes are in [ARCHITECTURE.md](./ARCHITECTURE.md).
|
||||
|
||||
+191
-119
@@ -3,78 +3,62 @@ package main
|
||||
import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/cristalhq/aconfig"
|
||||
"github.com/cristalhq/aconfig/aconfighcl"
|
||||
"github.com/cristalhq/aconfig/aconfigtoml"
|
||||
"github.com/cristalhq/aconfig/aconfigyaml"
|
||||
"github.com/lmittmann/tint"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sower-proxy/deferlog/log"
|
||||
"github.com/wweir/sower/pkg/suffixtree"
|
||||
"github.com/wweir/sower/router"
|
||||
"github.com/sower-proxy/deferlog/v2"
|
||||
"github.com/sower-proxy/sower/config"
|
||||
"github.com/sower-proxy/sower/pkg/suffixtree"
|
||||
"github.com/sower-proxy/sower/pkg/upstreamtls"
|
||||
"github.com/sower-proxy/sower/router"
|
||||
)
|
||||
|
||||
var (
|
||||
version, date string
|
||||
|
||||
conf = struct {
|
||||
Remote struct {
|
||||
Type string `default:"sower" required:"true" usage:"option: sower/trojan/socks5"`
|
||||
Addr string `required:"true" usage:"proxy address, eg: proxy.com/127.0.0.1:7890"`
|
||||
Password string `usage:"remote proxy password"`
|
||||
}
|
||||
|
||||
DNS struct {
|
||||
Disable bool `default:"false" usage:"disable DNS proxy"`
|
||||
Serve string `default:"127.0.0.1" required:"true" usage:"dns server ip"`
|
||||
Fallback string `default:"223.5.5.5" required:"true" usage:"fallback dns server"`
|
||||
}
|
||||
Socks5 struct {
|
||||
Disable bool `default:"false" usage:"disable sock5 proxy"`
|
||||
Addr string `default:"127.0.0.1:1080" usage:"socks5 listen address"`
|
||||
} `flag:"socks5"`
|
||||
|
||||
Router struct {
|
||||
Block struct {
|
||||
File string `usage:"block list file, local file or remote"`
|
||||
FilePrefix string `default:"**." usage:"parsed as '<prefix>line_text'"`
|
||||
Rules []string `usage:"block list rules"`
|
||||
}
|
||||
Direct struct {
|
||||
File string `usage:"direct list file, local file or remote"`
|
||||
FilePrefix string `default:"**." usage:"parsed as '<prefix>line_text'"`
|
||||
Rules []string `usage:"direct list rules"`
|
||||
}
|
||||
Proxy struct {
|
||||
File string `usage:"proxy list file, local file or remote"`
|
||||
FilePrefix string `default:"**." usage:"parsed as '<prefix>line_text'"`
|
||||
Rules []string `usage:"proxy list rules"`
|
||||
}
|
||||
|
||||
Country struct {
|
||||
MMDB string `usage:"mmdb file"`
|
||||
File string `usage:"CIDR block list file, local file or remote"`
|
||||
FilePrefix string `default:"" usage:"parsed as '<prefix>line_text'"`
|
||||
Rules []string `usage:"CIDR list rules"`
|
||||
}
|
||||
}
|
||||
}{}
|
||||
conf config.SowerConfig
|
||||
)
|
||||
|
||||
func init() {
|
||||
fi, _ := os.Stdout.Stat()
|
||||
noColor := (fi.Mode() & os.ModeCharDevice) == 0
|
||||
deferlog.SetDefault(slog.New(tint.NewHandler(os.Stdout,
|
||||
&tint.Options{AddSource: true, NoColor: noColor})))
|
||||
if strings.HasSuffix(os.Args[0], ".test") {
|
||||
return
|
||||
}
|
||||
|
||||
if err := aconfig.LoaderFor(&conf, aconfig.Config{
|
||||
AllowUnknownFields: true,
|
||||
FileFlag: "f",
|
||||
AllowUnknownFields: false,
|
||||
FileFlag: "c",
|
||||
Files: []string{
|
||||
"sower.hcl",
|
||||
"sower.toml",
|
||||
"sower.yaml",
|
||||
"sower.yml",
|
||||
"/etc/sower/sower.hcl",
|
||||
"/etc/sower/sower.toml",
|
||||
"/etc/sower/sower.yaml",
|
||||
"/etc/sower/sower.yml",
|
||||
},
|
||||
FileDecoders: map[string]aconfig.FileDecoder{
|
||||
".yml": aconfigyaml.New(),
|
||||
".yaml": aconfigyaml.New(),
|
||||
@@ -82,99 +66,158 @@ func init() {
|
||||
".hcl": aconfighcl.New(),
|
||||
},
|
||||
}).Load(); err != nil {
|
||||
log.Fatal().Err(err).
|
||||
Interface("config", conf).
|
||||
Msg("Load config")
|
||||
slog.Error("load config", "error", err, "config", conf)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
conf.Router.Direct.Rules = append(conf.Router.Direct.Rules,
|
||||
conf.Remote.Addr, "**.in-addr.arpa", "**.ip6.arpa")
|
||||
log.Info().
|
||||
Str("version", version).
|
||||
Str("date", date).
|
||||
Interface("config", conf).
|
||||
Msg("Starting")
|
||||
if err := conf.Validate(); err != nil {
|
||||
slog.Error("validate config", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
slog.Info("starting sower",
|
||||
"version", version,
|
||||
"date", date,
|
||||
"log_level", conf.LogLevel,
|
||||
"remote_type", conf.Remote.Type,
|
||||
"remote_addr", conf.Remote.Addr,
|
||||
"remote_password", deferlog.Secret(conf.Remote.Password),
|
||||
"remote_tls", conf.Remote.TLS,
|
||||
"dns", conf.DNS,
|
||||
"socks5", conf.Socks5,
|
||||
"router", conf.Router)
|
||||
}
|
||||
|
||||
func main() {
|
||||
proxyDial := GenProxyDial(conf.Remote.Type, conf.Remote.Addr, conf.Remote.Password)
|
||||
r := router.NewRouter(conf.DNS.Serve, conf.DNS.Fallback, conf.Router.Country.MMDB, proxyDial)
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
upstreamDNS := conf.DNS.Upstream
|
||||
if upstreamDNS == "" {
|
||||
upstreamDNS = conf.DNS.Fallback
|
||||
}
|
||||
proxyDial, err := GenProxyDial(conf.Remote.Type, conf.Remote.Addr, conf.Remote.Password, upstreamDNS, upstreamtls.Options{
|
||||
ServerName: conf.Remote.TLS.ServerName,
|
||||
ClientHello: conf.Remote.TLS.ClientHello,
|
||||
InsecureSkipVerify: conf.Remote.TLS.InsecureSkipVerify,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("build proxy dialer", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
r := router.NewRouter([]string{conf.DNS.Serve, conf.DNS.Serve6}, upstreamDNS, conf.DNS.Fallback, conf.Router.Country.MMDB, proxyDial)
|
||||
r.BlockRule = suffixtree.NewNodeFromRules(conf.Router.Block.Rules...)
|
||||
r.DirectRule = suffixtree.NewNodeFromRules(conf.Router.Direct.Rules...)
|
||||
r.ProxyRule = suffixtree.NewNodeFromRules(conf.Router.Proxy.Rules...)
|
||||
r.AddCountryCIDRs(conf.Router.Country.Rules...)
|
||||
|
||||
go func() {
|
||||
if conf.DNS.Disable {
|
||||
log.Info().Msg("DNS proxy disabled")
|
||||
return
|
||||
errCh := make(chan error, 8)
|
||||
|
||||
if conf.DNS.Disable {
|
||||
slog.Info("DNS proxy disabled")
|
||||
} else {
|
||||
ips := make([]string, 0, 2)
|
||||
if strings.TrimSpace(conf.DNS.Serve) != "" {
|
||||
ips = append(ips, conf.DNS.Serve)
|
||||
}
|
||||
if strings.TrimSpace(conf.DNS.Serve6) != "" {
|
||||
ips = append(ips, conf.DNS.Serve6)
|
||||
}
|
||||
|
||||
lnHTTP, err := net.Listen("tcp", net.JoinHostPort(conf.DNS.Serve, "80"))
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("listen port")
|
||||
}
|
||||
go ServeHTTP(lnHTTP, r)
|
||||
for _, ip := range ips {
|
||||
lnHTTP, err := net.Listen("tcp", net.JoinHostPort(ip, "80"))
|
||||
if err != nil {
|
||||
slog.Error("listen port 80", "error", err, "listen_on", net.JoinHostPort(ip, "80"))
|
||||
os.Exit(1)
|
||||
}
|
||||
go closeOnDone(ctx, lnHTTP)
|
||||
go reportServeError(errCh, "http proxy", ServeHTTP(ctx, lnHTTP, r))
|
||||
|
||||
lnHTTPS, err := net.Listen("tcp", net.JoinHostPort(conf.DNS.Serve, "443"))
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("listen port")
|
||||
}
|
||||
go ServeHTTPS(lnHTTPS, r)
|
||||
lnHTTPS, err := net.Listen("tcp", net.JoinHostPort(ip, "443"))
|
||||
if err != nil {
|
||||
slog.Error("listen port 443", "error", err, "listen_on", net.JoinHostPort(ip, "443"))
|
||||
os.Exit(1)
|
||||
}
|
||||
go closeOnDone(ctx, lnHTTPS)
|
||||
go reportServeError(errCh, "https proxy", ServeHTTPS(ctx, lnHTTPS, r))
|
||||
|
||||
log.Info().
|
||||
Str("listen_on", conf.DNS.Serve).
|
||||
Msg("DNS proxy started")
|
||||
if err := dns.ListenAndServe(net.JoinHostPort(conf.DNS.Serve, "53"), "udp", r); err != nil {
|
||||
log.Fatal().Err(err).Msg("serve dns")
|
||||
dnsServer := &dns.Server{
|
||||
Addr: net.JoinHostPort(ip, "53"),
|
||||
Net: "udp",
|
||||
Handler: r,
|
||||
}
|
||||
go shutdownDNSServerOnDone(ctx, dnsServer)
|
||||
go func(ip string, server *dns.Server) {
|
||||
err := server.ListenAndServe()
|
||||
if err != nil && !stderrors.Is(err, net.ErrClosed) {
|
||||
reportServeError(errCh, "dns proxy", fmt.Errorf("listen on %s: %w", ip, err))
|
||||
}
|
||||
}(ip, dnsServer)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
go func() {
|
||||
if conf.Socks5.Disable {
|
||||
log.Info().Msg("SOCKS5 proxy disabled")
|
||||
slog.Info("SOCKS5 proxy disabled")
|
||||
return
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", conf.Socks5.Addr)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("listen port")
|
||||
slog.Error("listen port", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Info().Msgf("SOCKS5 proxy listening on %s", conf.Socks5.Addr)
|
||||
go ServeSocks5(ln, r)
|
||||
slog.Info("SOCKS5 proxy listening", "addr", conf.Socks5.Addr)
|
||||
go closeOnDone(ctx, ln)
|
||||
reportServeError(errCh, "socks5 proxy", ServeSocks5(ctx, ln, r))
|
||||
}()
|
||||
|
||||
start := time.Now()
|
||||
loadRule(r.BlockRule, proxyDial, conf.Router.Block.File, conf.Router.Block.FilePrefix)
|
||||
loadRule(r.DirectRule, proxyDial, conf.Router.Direct.File, conf.Router.Direct.FilePrefix)
|
||||
loadRule(r.ProxyRule, proxyDial, conf.Router.Proxy.File, conf.Router.Proxy.FilePrefix)
|
||||
for line := range featchRuleFile(proxyDial, conf.Router.Country.File) {
|
||||
loadRule(ctx, r.BlockRule, proxyDial, conf.Router.Block.File, conf.Router.Block.FilePrefix)
|
||||
loadRule(ctx, r.DirectRule, proxyDial, conf.Router.Direct.File, conf.Router.Direct.FilePrefix)
|
||||
loadRule(ctx, r.ProxyRule, proxyDial, conf.Router.Proxy.File, conf.Router.Proxy.FilePrefix)
|
||||
for line := range fetchRuleFile(ctx, proxyDial, conf.Router.Country.File) {
|
||||
r.AddCountryCIDRs(line)
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Dur("took", time.Since(start)).
|
||||
Uint64("blockRule", r.BlockRule.Count).
|
||||
Uint64("directRule", r.DirectRule.Count).
|
||||
Uint64("proxyRule", r.ProxyRule.Count).
|
||||
Msg("Loaded rules, proxy started")
|
||||
slog.Info("loaded rules, proxy started", "took", time.Since(start),
|
||||
"blockRule", r.BlockRule.Count, "directRule", r.DirectRule.Count, "proxyRule", r.ProxyRule.Count)
|
||||
runtime.GC()
|
||||
select {}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
slog.Info("shutting down sower", "reason", ctx.Err())
|
||||
case err := <-errCh:
|
||||
slog.Error("serve failed", "error", err)
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
func loadRule(rule *suffixtree.Node, proxyDial router.ProxyDialFn, file, linePrefix string) {
|
||||
for line := range featchRuleFile(proxyDial, file) {
|
||||
func loadRule(ctx context.Context, rule *suffixtree.Node, proxyDial router.ProxyDialFn, file, linePrefix string) {
|
||||
for line := range fetchRuleFile(ctx, proxyDial, file) {
|
||||
rule.Add(linePrefix + line)
|
||||
}
|
||||
rule.GC()
|
||||
}
|
||||
func featchRuleFile(proxyDial router.ProxyDialFn, file string) <-chan string {
|
||||
|
||||
func fetchRuleFile(ctx context.Context, proxyDial router.ProxyDialFn, file string) <-chan string {
|
||||
if file == "" {
|
||||
ch := make(chan string)
|
||||
close(ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
var loadFn func() (io.ReadCloser, error)
|
||||
if _, err := url.Parse(file); err == nil {
|
||||
if _, err := os.Stat(file); err == nil {
|
||||
// load rule file from local file
|
||||
loadFn = func() (io.ReadCloser, error) {
|
||||
return os.Open(file)
|
||||
}
|
||||
} else {
|
||||
// load rule file from remote by HTTP
|
||||
client := &http.Client{
|
||||
Timeout: 15 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
Dial: func(network, addr string) (net.Conn, error) {
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
domain, port, _ := net.SplitHostPort(addr)
|
||||
p, _ := strconv.Atoi(port)
|
||||
return proxyDial("tcp", domain, uint16(p))
|
||||
@@ -183,7 +226,10 @@ func featchRuleFile(proxyDial router.ProxyDialFn, file string) <-chan string {
|
||||
}
|
||||
|
||||
loadFn = func() (io.ReadCloser, error) {
|
||||
req, _ := http.NewRequest(http.MethodGet, file, nil)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, file, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("Accept-Encoding", "gzip")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
@@ -197,12 +243,6 @@ func featchRuleFile(proxyDial router.ProxyDialFn, file string) <-chan string {
|
||||
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
} else {
|
||||
// load rule file from local file
|
||||
loadFn = func() (io.ReadCloser, error) {
|
||||
return os.Open(file)
|
||||
}
|
||||
}
|
||||
|
||||
// load rule file, retry 10 times
|
||||
@@ -211,32 +251,43 @@ func featchRuleFile(proxyDial router.ProxyDialFn, file string) <-chan string {
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
slog.Error("fetch rule file", "error", ctx.Err(), "file", file)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// wait: 28.5s
|
||||
time.Sleep(i * i * 100 * time.Millisecond)
|
||||
timer := time.NewTimer(i * i * 100 * time.Millisecond)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
slog.Error("fetch rule file", "error", ctx.Err(), "file", file)
|
||||
os.Exit(1)
|
||||
case <-timer.C:
|
||||
}
|
||||
rc, err = loadFn()
|
||||
}
|
||||
log.InfoFatal(err).
|
||||
Str("file", file).
|
||||
Msg("fetch rule file")
|
||||
if err != nil {
|
||||
slog.Error("fetch rule file", "error", err, "file", file)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ch := make(chan string, 100)
|
||||
go func() {
|
||||
defer rc.Close()
|
||||
defer close(ch)
|
||||
gr, err := gzip.NewReader(rc)
|
||||
log.DebugFatal(err).Msg("gzip reader")
|
||||
defer gr.Close()
|
||||
if gr, err := gzip.NewReader(rc); err == nil {
|
||||
rc = gr
|
||||
defer gr.Close()
|
||||
}
|
||||
|
||||
br := bufio.NewReader(gr)
|
||||
br := bufio.NewReader(rc)
|
||||
for {
|
||||
line, _, err := br.ReadLine()
|
||||
if err == io.EOF {
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Error().Err(err).
|
||||
Str("file", file).
|
||||
Msg("read line")
|
||||
slog.Error("read line", "error", err, "file", file)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -250,3 +301,24 @@ func featchRuleFile(proxyDial router.ProxyDialFn, file string) <-chan string {
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
func closeOnDone(ctx context.Context, closer io.Closer) {
|
||||
<-ctx.Done()
|
||||
_ = closer.Close()
|
||||
}
|
||||
|
||||
func shutdownDNSServerOnDone(ctx context.Context, server *dns.Server) {
|
||||
<-ctx.Done()
|
||||
_ = server.ShutdownContext(ctx)
|
||||
}
|
||||
|
||||
func reportServeError(errCh chan<- error, service string, err error) {
|
||||
if err == nil || stderrors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case errCh <- fmt.Errorf("%s: %w", service, err):
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFetchRuleFileEmptyPathReturnsClosedChannel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ch := fetchRuleFile(context.Background(), nil, "")
|
||||
if _, ok := <-ch; ok {
|
||||
t.Fatal("expected closed channel for empty rule file path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchRuleFileRemoteGzip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
gz := gzip.NewWriter(w)
|
||||
defer gz.Close()
|
||||
_, _ = gz.Write([]byte("alpha\n\nbeta\n"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
dialFn := func(network, host string, port uint16) (net.Conn, error) {
|
||||
return net.Dial(network, net.JoinHostPort(host, strconv.Itoa(int(port))))
|
||||
}
|
||||
|
||||
got := make([]string, 0, 2)
|
||||
for line := range fetchRuleFile(context.Background(), dialFn, srv.URL) {
|
||||
got = append(got, line)
|
||||
}
|
||||
|
||||
want := []string{"alpha", "beta"}
|
||||
if !slices.Equal(got, want) {
|
||||
t.Fatalf("unexpected rules: got %v want %v", got, want)
|
||||
}
|
||||
}
|
||||
+259
-81
@@ -2,56 +2,76 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sower-proxy/conns/relay"
|
||||
"github.com/sower-proxy/conns/teeconn"
|
||||
"github.com/sower-proxy/deferlog/log"
|
||||
"github.com/wweir/sower/router"
|
||||
"github.com/wweir/sower/transport"
|
||||
"github.com/wweir/sower/transport/socks5"
|
||||
"github.com/wweir/sower/transport/sower"
|
||||
"github.com/wweir/sower/transport/trojan"
|
||||
"github.com/sower-proxy/conns/reread"
|
||||
"github.com/sower-proxy/sower/pkg/upstreamtls"
|
||||
"github.com/sower-proxy/sower/router"
|
||||
"github.com/sower-proxy/sower/transport"
|
||||
"github.com/sower-proxy/sower/transport/socks5"
|
||||
"github.com/sower-proxy/sower/transport/sower"
|
||||
"github.com/sower-proxy/sower/transport/trojan"
|
||||
)
|
||||
|
||||
func GenProxyDial(proxyType, proxyHost, proxyPassword string) router.ProxyDialFn {
|
||||
const (
|
||||
proxyDialTimeout = 10 * time.Second
|
||||
proxyReadTimeout = 15 * time.Second
|
||||
)
|
||||
|
||||
func GenProxyDial(proxyType, proxyHost, proxyPassword, dns string, tlsOptions upstreamtls.Options) (router.ProxyDialFn, error) {
|
||||
var proxy transport.Transport
|
||||
var dialFn func() (net.Conn, error)
|
||||
|
||||
switch conf.Remote.Type {
|
||||
case "sower":
|
||||
proxy = sower.New(conf.Remote.Password)
|
||||
tlsCfg := &tls.Config{}
|
||||
dialFn = func() (net.Conn, error) {
|
||||
return tls.Dial("tcp", net.JoinHostPort(proxyHost, "443"), tlsCfg)
|
||||
}
|
||||
dialer := &net.Dialer{
|
||||
Timeout: proxyDialTimeout,
|
||||
KeepAlive: 30 * time.Second,
|
||||
Resolver: &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
d := net.Dialer{Timeout: proxyDialTimeout}
|
||||
c, err := d.DialContext(ctx, "udp", net.JoinHostPort(dns, "53"))
|
||||
if err != nil {
|
||||
slog.Warn("dial fallback dns failed, use default dns setting", "error", err)
|
||||
c, err = d.DialContext(ctx, network, address)
|
||||
}
|
||||
return c, err
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
case "trojan":
|
||||
proxy = trojan.New(conf.Remote.Password)
|
||||
tlsCfg := &tls.Config{}
|
||||
dialFn = func() (net.Conn, error) {
|
||||
return tls.Dial("tcp", net.JoinHostPort(proxyHost, "443"), tlsCfg)
|
||||
switch proxyType {
|
||||
case "sower", "trojan":
|
||||
tlsDialFn, err := newTLSDialFn(dialer, proxyHost, tlsOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if proxyType == "sower" {
|
||||
proxy = sower.New(proxyPassword)
|
||||
} else {
|
||||
proxy = trojan.New(proxyPassword)
|
||||
}
|
||||
dialFn = tlsDialFn
|
||||
case "socks5":
|
||||
proxy = socks5.New()
|
||||
dialFn = func() (net.Conn, error) {
|
||||
return net.Dial("tcp", proxyHost)
|
||||
return dialer.Dial("tcp", proxyHost)
|
||||
}
|
||||
|
||||
default:
|
||||
log.Fatal().
|
||||
Str("type", conf.Remote.Type).
|
||||
Msg("unknown proxy type")
|
||||
return nil, fmt.Errorf("unknown proxy type %q", proxyType)
|
||||
}
|
||||
|
||||
return func(network, host string, port uint16) (net.Conn, error) {
|
||||
if host == "" || port == 0 {
|
||||
return nil, errors.Errorf("invalid addr(%s:%d)", host, port)
|
||||
return nil, fmt.Errorf("invalid addr(%s:%d)", host, port)
|
||||
}
|
||||
|
||||
conn, err := dialFn()
|
||||
@@ -65,98 +85,256 @@ func GenProxyDial(proxyType, proxyHost, proxyPassword string) router.ProxyDialFn
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newTLSDialFn(dialer *net.Dialer, proxyHost string, tlsOptions upstreamtls.Options) (func() (net.Conn, error), error) {
|
||||
dialAddr, err := upstreamDialAddr(proxyHost, "443")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tlsOptions.ClientHello != "" {
|
||||
if err := upstreamtls.ValidateClientHello(tlsOptions.ClientHello); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return func() (net.Conn, error) {
|
||||
return upstreamtls.Dial(dialer, "tcp", dialAddr, tlsOptions)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func upstreamDialAddr(addr, defaultPort string) (string, error) {
|
||||
if host, port, err := net.SplitHostPort(addr); err == nil {
|
||||
if host == "" || port == "" {
|
||||
return "", fmt.Errorf("invalid remote address %q", addr)
|
||||
}
|
||||
return net.JoinHostPort(host, port), nil
|
||||
}
|
||||
|
||||
if addr == "" {
|
||||
return "", fmt.Errorf("empty remote address")
|
||||
}
|
||||
if strings.HasPrefix(addr, "[") || strings.HasSuffix(addr, "]") {
|
||||
return "", fmt.Errorf("invalid remote address %q", addr)
|
||||
}
|
||||
if strings.Count(addr, ":") == 1 {
|
||||
return "", fmt.Errorf("missing or invalid port in %q", addr)
|
||||
}
|
||||
return net.JoinHostPort(addr, defaultPort), nil
|
||||
}
|
||||
|
||||
func ServeHTTP(ctx context.Context, ln net.Listener, r *router.Router) error {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
if shouldRetryAccept(ctx, "http", err) {
|
||||
continue
|
||||
}
|
||||
return wrapAcceptErr(ctx, "http", err)
|
||||
}
|
||||
go handleHTTPConn(conn, r)
|
||||
}
|
||||
}
|
||||
|
||||
func ServeHTTP(ln net.Listener, r *router.Router) {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).
|
||||
Msg("serve socks5")
|
||||
func ServeHTTPS(ctx context.Context, ln net.Listener, r *router.Router) error {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
if shouldRetryAccept(ctx, "https", err) {
|
||||
continue
|
||||
}
|
||||
return wrapAcceptErr(ctx, "https", err)
|
||||
}
|
||||
go handleHTTPSConn(conn, r)
|
||||
}
|
||||
}
|
||||
|
||||
go ServeHTTP(ln, r)
|
||||
func ServeSocks5(ctx context.Context, ln net.Listener, r *router.Router) error {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
if shouldRetryAccept(ctx, "socks5", err) {
|
||||
continue
|
||||
}
|
||||
return wrapAcceptErr(ctx, "socks5", err)
|
||||
}
|
||||
go handleSocks5Conn(conn, r)
|
||||
}
|
||||
}
|
||||
|
||||
func handleHTTPConn(conn net.Conn, r *router.Router) {
|
||||
start := time.Now()
|
||||
teeconn := teeconn.New(conn)
|
||||
defer teeconn.Close()
|
||||
rereadConn := reread.New(conn)
|
||||
defer rereadConn.Close()
|
||||
|
||||
req, err := http.ReadRequest(bufio.NewReader(teeconn))
|
||||
_ = rereadConn.SetDeadline(time.Now().Add(proxyReadTimeout))
|
||||
req, err := http.ReadRequest(bufio.NewReader(rereadConn))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("read http request")
|
||||
slog.Error("read http request", "error", err)
|
||||
return
|
||||
}
|
||||
_ = rereadConn.SetDeadline(time.Time{})
|
||||
|
||||
rc, err := r.ProxyDial("tcp", req.Host, 80)
|
||||
if err != nil {
|
||||
log.Error().Err(err).
|
||||
Str("host", req.Host).
|
||||
Interface("req", req.URL).
|
||||
Msg("dial proxy")
|
||||
slog.Error("dial proxy", "error", err, "host", req.Host, "req", req.URL)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
teeconn.Stop().Reread()
|
||||
err = relay.Relay(teeconn, rc)
|
||||
log.DebugWarn(err).
|
||||
Str("host", req.Host).
|
||||
Dur("spend", time.Since(start)).
|
||||
Msg("serve http")
|
||||
rereadConn.Stop().Reread()
|
||||
err = relay.Relay(rereadConn, rc)
|
||||
if err != nil {
|
||||
slog.Debug("serve http", "error", err, "host", req.Host, "spend", time.Since(start))
|
||||
}
|
||||
}
|
||||
|
||||
func ServeHTTPS(ln net.Listener, r *router.Router) {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).
|
||||
Msg("serve socks5")
|
||||
}
|
||||
|
||||
go ServeHTTPS(ln, r)
|
||||
func handleHTTPSConn(conn net.Conn, r *router.Router) {
|
||||
start := time.Now()
|
||||
teeconn := teeconn.New(conn)
|
||||
defer teeconn.Close()
|
||||
rereadConn := reread.New(conn)
|
||||
defer rereadConn.Close()
|
||||
|
||||
_ = rereadConn.SetDeadline(time.Now().Add(proxyReadTimeout))
|
||||
|
||||
var domain string
|
||||
tls.Server(teeconn, &tls.Config{
|
||||
err := tls.Server(rereadConn, &tls.Config{
|
||||
GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
domain = hello.ServerName
|
||||
return nil, nil
|
||||
},
|
||||
}).Handshake()
|
||||
if err != nil {
|
||||
slog.Debug("tls handshake failed", "error", err)
|
||||
return
|
||||
}
|
||||
if domain == "" {
|
||||
slog.Debug("tls handshake missing server name")
|
||||
return
|
||||
}
|
||||
_ = rereadConn.SetDeadline(time.Time{})
|
||||
|
||||
rc, err := r.ProxyDial("tcp", domain, 443)
|
||||
if err != nil {
|
||||
log.Error().Err(err).
|
||||
Str("host", domain).
|
||||
Msg("dial proxy")
|
||||
slog.Error("dial proxy", "error", err, "host", domain)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
teeconn.Stop().Reread()
|
||||
err = relay.Relay(teeconn, rc)
|
||||
log.DebugWarn(err).
|
||||
Str("host", domain).
|
||||
Dur("spend", time.Since(start)).
|
||||
Msg("serve http")
|
||||
rereadConn.Stop().Reread()
|
||||
err = relay.Relay(rereadConn, rc)
|
||||
if err != nil {
|
||||
slog.Debug("serve https", "error", err, "host", domain, "spend", time.Since(start))
|
||||
}
|
||||
}
|
||||
|
||||
func ServeSocks5(ln net.Listener, r *router.Router) {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).
|
||||
Msg("serve socks5")
|
||||
}
|
||||
go ServeSocks5(ln, r)
|
||||
func handleSocks5Conn(conn net.Conn, r *router.Router) {
|
||||
defer conn.Close()
|
||||
|
||||
addr, err := socks5.New().Unwrap(conn)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).
|
||||
Msgf("parse socks5 target: %s", addr)
|
||||
rereadConn := reread.New(conn)
|
||||
_ = rereadConn.SetDeadline(time.Now().Add(proxyReadTimeout))
|
||||
|
||||
byte1 := make([]byte, 1)
|
||||
if n, err := rereadConn.Read(byte1); err != nil || n != 1 {
|
||||
slog.Error("read first byte", "error", err)
|
||||
return
|
||||
}
|
||||
rereadConn.Reread()
|
||||
_ = rereadConn.SetDeadline(time.Time{})
|
||||
|
||||
if byte1[0] == 5 {
|
||||
rereadConn.Stop()
|
||||
server := socks5.New()
|
||||
addr, err := server.ReadRequest(rereadConn)
|
||||
if err != nil {
|
||||
slog.Error("read socks5 request", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
host, port := addr.(*socks5.AddrHead).Addr()
|
||||
rc, err := r.Dial("tcp", host, port)
|
||||
if err != nil {
|
||||
if replyErr := server.WriteReply(rereadConn, routeSocks5ReplyCode(err)); replyErr != nil {
|
||||
slog.Debug("write socks5 failure reply", "error", replyErr, "host", host, "port", port)
|
||||
}
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
if err := server.WriteReply(rereadConn, socks5.RepSucceeded); err != nil {
|
||||
slog.Debug("write socks5 success reply", "error", err, "host", host, "port", port)
|
||||
return
|
||||
}
|
||||
_ = relay.Relay(rereadConn, rc)
|
||||
return
|
||||
}
|
||||
|
||||
host, port := addr.(*socks5.AddrHead).Addr()
|
||||
r.RouteHandle(conn, host, port)
|
||||
req, err := http.ReadRequest(bufio.NewReader(rereadConn))
|
||||
if err != nil {
|
||||
slog.Error("read http request", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
host, port, err := router.ParseHostPort(req.Host, req.URL)
|
||||
if err != nil {
|
||||
rereadConn.Stop().Write([]byte("HTTP/1.1 400 Bad Request\r\n\r\n"))
|
||||
return
|
||||
}
|
||||
|
||||
rc, err := r.Dial("tcp", host, port)
|
||||
if err != nil {
|
||||
writeHTTPProxyError(rereadConn.Stop(), err)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
if req.Method == http.MethodConnect {
|
||||
rereadConn.Stop().Reset()
|
||||
if _, err := rereadConn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n")); err != nil {
|
||||
slog.Debug("write connect response", "error", err, "host", host, "port", port)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
rereadConn.Stop().Reread()
|
||||
}
|
||||
|
||||
if err := relay.Relay(rereadConn, rc); err != nil {
|
||||
slog.Debug("serve proxy request", "error", err, "host", host, "port", port, "method", req.Method)
|
||||
}
|
||||
}
|
||||
|
||||
func shouldRetryAccept(ctx context.Context, protocol string, err error) bool {
|
||||
if err == nil || ctx.Err() != nil {
|
||||
return false
|
||||
}
|
||||
if ne, ok := err.(net.Error); ok && ne.Temporary() {
|
||||
slog.Warn("temporary accept failed", "protocol", protocol, "error", err)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func wrapAcceptErr(ctx context.Context, protocol string, err error) error {
|
||||
if err == nil || ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("accept %s connection: %w", protocol, err)
|
||||
}
|
||||
|
||||
func writeHTTPProxyError(conn net.Conn, err error) {
|
||||
status := "502 Bad Gateway"
|
||||
if stderrors.Is(err, router.ErrBlocked) {
|
||||
status = "403 Forbidden"
|
||||
}
|
||||
if _, writeErr := conn.Write([]byte("HTTP/1.1 " + status + "\r\n\r\n")); writeErr != nil {
|
||||
slog.Debug("write http proxy error", "error", writeErr, "status", status)
|
||||
}
|
||||
}
|
||||
|
||||
func routeSocks5ReplyCode(err error) byte {
|
||||
if stderrors.Is(err, router.ErrBlocked) {
|
||||
return socks5.RepConnectionNotAllowed
|
||||
}
|
||||
return socks5.RepGeneralFailure
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sower-proxy/sower/pkg/suffixtree"
|
||||
"github.com/sower-proxy/sower/pkg/upstreamtls"
|
||||
"github.com/sower-proxy/sower/router"
|
||||
)
|
||||
|
||||
func TestGenProxyDialRejectsUnknownProxyType(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if _, err := GenProxyDial("unknown", "example.com", "", "8.8.8.8", upstreamtls.Options{}); err == nil {
|
||||
t.Fatal("expected error for unknown proxy type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenProxyDialRejectsInvalidTLSClientHello(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := GenProxyDial("sower", "example.com", "", "8.8.8.8", upstreamtls.Options{
|
||||
ClientHello: "invalid",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid TLS client hello")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpstreamDialAddrAddsDefaultPort(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
addr, err := upstreamDialAddr("example.com", "443")
|
||||
if err != nil {
|
||||
t.Fatalf("upstream dial addr: %v", err)
|
||||
}
|
||||
if addr != "example.com:443" {
|
||||
t.Fatalf("unexpected dial addr: %q", addr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpstreamDialAddrPreservesExplicitPort(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
addr, err := upstreamDialAddr("example.com:8443", "443")
|
||||
if err != nil {
|
||||
t.Fatalf("upstream dial addr: %v", err)
|
||||
}
|
||||
if addr != "example.com:8443" {
|
||||
t.Fatalf("unexpected dial addr: %q", addr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleSocks5ConnReturnsForbiddenForBlockedConnect(t *testing.T) {
|
||||
server, client := net.Pipe()
|
||||
defer client.Close()
|
||||
|
||||
r := newTestRouter()
|
||||
go handleSocks5Conn(server, r)
|
||||
|
||||
client.SetDeadline(time.Now().Add(2 * time.Second))
|
||||
if _, err := io.WriteString(client, "CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n"); err != nil {
|
||||
t.Fatalf("write request: %v", err)
|
||||
}
|
||||
|
||||
resp, err := io.ReadAll(client)
|
||||
if err != nil {
|
||||
t.Fatalf("read response: %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(string(resp), "HTTP/1.1 403 Forbidden") {
|
||||
t.Fatalf("unexpected response: %q", string(resp))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleSocks5ConnReturnsSocks5FailureForBlockedRequest(t *testing.T) {
|
||||
server, client := net.Pipe()
|
||||
defer client.Close()
|
||||
|
||||
r := newTestRouter()
|
||||
go handleSocks5Conn(server, r)
|
||||
|
||||
client.SetDeadline(time.Now().Add(2 * time.Second))
|
||||
if _, err := client.Write([]byte{0x05, 0x01, 0x00}); err != nil {
|
||||
t.Fatalf("write auth request: %v", err)
|
||||
}
|
||||
|
||||
authResp := make([]byte, 2)
|
||||
if _, err := io.ReadFull(client, authResp); err != nil {
|
||||
t.Fatalf("read auth response: %v", err)
|
||||
}
|
||||
if authResp[0] != 0x05 || authResp[1] != 0x00 {
|
||||
t.Fatalf("unexpected auth response: %v", authResp)
|
||||
}
|
||||
|
||||
req := []byte{0x05, 0x01, 0x00, 0x03, 0x0b}
|
||||
req = append(req, []byte("example.com")...)
|
||||
req = append(req, 0x01, 0xbb)
|
||||
if _, err := client.Write(req); err != nil {
|
||||
t.Fatalf("write connect request: %v", err)
|
||||
}
|
||||
|
||||
reply := make([]byte, 10)
|
||||
if _, err := io.ReadFull(client, reply); err != nil {
|
||||
t.Fatalf("read connect reply: %v", err)
|
||||
}
|
||||
if reply[1] != 0x02 {
|
||||
t.Fatalf("expected connection-not-allowed reply, got %d", reply[1])
|
||||
}
|
||||
}
|
||||
|
||||
func newTestRouter() *router.Router {
|
||||
return &router.Router{
|
||||
BlockRule: suffixtree.NewNodeFromRules("example.com"),
|
||||
DirectRule: suffixtree.NewNodeFromRules(),
|
||||
ProxyRule: suffixtree.NewNodeFromRules(),
|
||||
ProxyDial: func(network, host string, port uint16) (net.Conn, error) {
|
||||
return nil, router.ErrBlocked
|
||||
},
|
||||
}
|
||||
}
|
||||
+304
-96
@@ -1,60 +1,195 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/cristalhq/aconfig"
|
||||
"github.com/cristalhq/aconfig/aconfigtoml"
|
||||
"github.com/cristalhq/aconfig/aconfigyaml"
|
||||
"github.com/lmittmann/tint"
|
||||
"github.com/sower-proxy/conns/relay"
|
||||
"github.com/sower-proxy/conns/teeconn"
|
||||
"github.com/sower-proxy/deferlog"
|
||||
"github.com/sower-proxy/deferlog/log"
|
||||
"github.com/wweir/sower/transport/sower"
|
||||
"github.com/wweir/sower/transport/trojan"
|
||||
"github.com/sower-proxy/conns/reread"
|
||||
"github.com/sower-proxy/deferlog/v2"
|
||||
"github.com/sower-proxy/sower/config"
|
||||
"github.com/sower-proxy/sower/internal/install"
|
||||
transportSower "github.com/sower-proxy/sower/transport/sower"
|
||||
"github.com/sower-proxy/sower/transport/trojan"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
const (
|
||||
httpShutdownTimeout = 5 * time.Second
|
||||
probeTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
version, date string
|
||||
|
||||
conf = struct {
|
||||
ServeIP string `usage:"listen to port 80 443 of this IP, eg: 0.0.0.0"`
|
||||
Password string `required:"true"`
|
||||
FakeSite string `default:"127.0.0.1:8080" usage:"fake site address or directoy"`
|
||||
|
||||
Cert struct {
|
||||
Email string
|
||||
Cert string
|
||||
Key string
|
||||
}
|
||||
}{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
err := aconfig.LoaderFor(&conf, aconfig.Config{}).Load()
|
||||
log.InfoFatal(err).
|
||||
Str("version", version).
|
||||
Str("date", date).
|
||||
Interface("config", conf).
|
||||
Msg("Load config")
|
||||
setLogger(slog.LevelInfo)
|
||||
}
|
||||
|
||||
func main() {
|
||||
cacheDir, _ := os.UserCacheDir()
|
||||
cacheDir = filepath.Join(cacheDir, "sower")
|
||||
if err := os.MkdirAll(cacheDir, 0600); err != nil {
|
||||
log.Fatal().Err(err).
|
||||
Str("dir", cacheDir).
|
||||
Msg("make cache dir")
|
||||
if hasInstallFlag(os.Args[1:]) {
|
||||
if err := install.InstallService(stdinConfirm); err != nil {
|
||||
slog.Error("install service", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
certManager := autocert.Manager{
|
||||
conf, err := loadConfig()
|
||||
if err != nil {
|
||||
slog.Error("load config", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
setLogger(conf.LogLevel)
|
||||
slog.Info("load config",
|
||||
"version", version,
|
||||
"date", date,
|
||||
"config", sanitizeConfig(conf))
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
if err := run(ctx, conf); err != nil {
|
||||
slog.Error("sowerd exited", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfig() (config.SowerdConfig, error) {
|
||||
var conf config.SowerdConfig
|
||||
if err := aconfig.LoaderFor(&conf, aconfig.Config{
|
||||
AllowUnknownFlags: false,
|
||||
AllowUnknownFields: false,
|
||||
FileFlag: "c",
|
||||
Files: []string{
|
||||
"sowerd.toml",
|
||||
"sowerd.yaml",
|
||||
"sowerd.yml",
|
||||
"config/sowerd.toml",
|
||||
"config/sowerd.yaml",
|
||||
"config/sowerd.yml",
|
||||
"/etc/sower/sowerd.toml",
|
||||
"/etc/sower/sowerd.yaml",
|
||||
"/etc/sower/sowerd.yml",
|
||||
},
|
||||
FileDecoders: map[string]aconfig.FileDecoder{
|
||||
".yml": aconfigyaml.New(),
|
||||
".yaml": aconfigyaml.New(),
|
||||
".toml": aconfigtoml.New(),
|
||||
},
|
||||
}).Load(); err != nil {
|
||||
return config.SowerdConfig{}, err
|
||||
}
|
||||
if err := conf.Validate(); err != nil {
|
||||
return config.SowerdConfig{}, err
|
||||
}
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
func run(ctx context.Context, conf config.SowerdConfig) error {
|
||||
cacheDir, err := cacheDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
certManager, tlsConf, err := buildTLSConfig(cacheDir, conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fakeSite, dirServer, err := prepareFakeSite(conf.FakeSite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpAddr := net.JoinHostPort(conf.ServeIP, "80")
|
||||
httpServer := &http.Server{
|
||||
Addr: httpAddr,
|
||||
Handler: certManager.HTTPHandler(fakeSiteHandler(dirServer)),
|
||||
}
|
||||
|
||||
httpErrCh := make(chan error, 1)
|
||||
go func() {
|
||||
err := httpServer.ListenAndServe()
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
httpErrCh <- fmt.Errorf("listen http %s: %w", httpAddr, err)
|
||||
}
|
||||
close(httpErrCh)
|
||||
}()
|
||||
|
||||
httpsAddr := net.JoinHostPort(conf.ServeIP, "443")
|
||||
ln, err := tls.Listen("tcp", httpsAddr, tlsConf)
|
||||
if err != nil {
|
||||
_ = shutdownHTTP(context.Background(), httpServer)
|
||||
return fmt.Errorf("listen https %s: %w", httpsAddr, err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
httpsErrCh := make(chan error, 1)
|
||||
go func() {
|
||||
httpsErrCh <- serve443(ctx, ln, fakeSite, transportSower.New(conf.Password), trojan.New(conf.Password))
|
||||
close(httpsErrCh)
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-httpErrCh:
|
||||
if err != nil {
|
||||
_ = ln.Close()
|
||||
return err
|
||||
}
|
||||
case err := <-httpsErrCh:
|
||||
if err != nil {
|
||||
_ = shutdownHTTP(context.Background(), httpServer)
|
||||
return err
|
||||
}
|
||||
case <-ctx.Done():
|
||||
}
|
||||
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), httpShutdownTimeout)
|
||||
defer cancel()
|
||||
|
||||
_ = ln.Close()
|
||||
if err := shutdownHTTP(shutdownCtx, httpServer); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cacheDir() (string, error) {
|
||||
base, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get user cache dir: %w", err)
|
||||
}
|
||||
|
||||
dir := filepath.Join(base, "sower")
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
return "", fmt.Errorf("create cache dir: %w", err)
|
||||
}
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
func buildTLSConfig(cacheDir string, cfg config.SowerdConfig) (*autocert.Manager, *tls.Config, error) {
|
||||
certManager := &autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
Email: conf.Cert.Email,
|
||||
Email: cfg.Cert.Email,
|
||||
Cache: autocert.DirCache(cacheDir),
|
||||
}
|
||||
|
||||
@@ -63,86 +198,159 @@ func main() {
|
||||
MinVersion: tls.VersionTLS12,
|
||||
NextProtos: []string{"http/1.1", "h2"},
|
||||
}
|
||||
if conf.Cert.Cert != "" || conf.Cert.Key != "" {
|
||||
cert, err := tls.LoadX509KeyPair(conf.Cert.Cert, conf.Cert.Key)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("load certificate")
|
||||
|
||||
if cfg.Cert.Cert == "" {
|
||||
return certManager, tlsConf, nil
|
||||
}
|
||||
|
||||
cert, err := tls.LoadX509KeyPair(cfg.Cert.Cert, cfg.Cert.Key)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("load cert pair: %w", err)
|
||||
}
|
||||
|
||||
tlsConf.GetCertificate = nil
|
||||
tlsConf.Certificates = []tls.Certificate{cert}
|
||||
return certManager, tlsConf, nil
|
||||
}
|
||||
|
||||
func prepareFakeSite(fakeSite string) (string, http.Handler, error) {
|
||||
si, err := os.Stat(fakeSite)
|
||||
if err == nil && si.IsDir() {
|
||||
slog.Info("serve fake site on http", "dir", fakeSite)
|
||||
return "127.0.0.1:80", http.FileServer(http.Dir(fakeSite)), nil
|
||||
}
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return "", nil, fmt.Errorf("stat fake site: %w", err)
|
||||
}
|
||||
return fakeSite, nil, nil
|
||||
}
|
||||
|
||||
func fakeSiteHandler(dirServer http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if dirServer != nil && isLoopbackRemoteAddr(r.RemoteAddr) {
|
||||
dirServer.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
tlsConf.GetCertificate = nil
|
||||
tlsConf.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
|
||||
// Redirect 80 to 443
|
||||
go func() {
|
||||
err := http.ListenAndServe(net.JoinHostPort(conf.ServeIP, "80"),
|
||||
certManager.HTTPHandler(http.HandlerFunc(redirectToHTTPS)))
|
||||
log.Fatal().Err(err).
|
||||
Str("IP", conf.ServeIP).
|
||||
Msg("Listen HTTP service")
|
||||
}()
|
||||
|
||||
ln, err := tls.Listen("tcp", net.JoinHostPort(conf.ServeIP, "443"), tlsConf)
|
||||
log.InfoFatal(err).
|
||||
Str("IP", conf.ServeIP).
|
||||
Msg("Start listen HTTPS service")
|
||||
|
||||
if si, err := os.Stat(conf.FakeSite); err == nil && si.IsDir() {
|
||||
http.NewFileTransport(http.Dir(conf.FakeSite))
|
||||
} else if _, _, err := net.SplitHostPort(conf.FakeSite); err == nil {
|
||||
go serve443(ln, conf.FakeSite, sower.New(conf.Password), trojan.New(conf.Password))
|
||||
}
|
||||
|
||||
select {}
|
||||
target := "https://" + r.Host + r.URL.RequestURI()
|
||||
http.Redirect(w, r, target, http.StatusPermanentRedirect)
|
||||
})
|
||||
}
|
||||
|
||||
func redirectToHTTPS(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Scheme = "https"
|
||||
if host, _, err := net.SplitHostPort(r.Host); err != nil {
|
||||
r.URL.Host = r.Host
|
||||
} else {
|
||||
r.URL.Host = host
|
||||
}
|
||||
func serve443(ctx context.Context, ln net.Listener, fakeSite string, sower *transportSower.Sower, trojan *trojan.Trojan) error {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
if ctx.Err() != nil || errors.Is(err, net.ErrClosed) {
|
||||
return nil
|
||||
}
|
||||
|
||||
http.Redirect(w, r, r.URL.String(), 301)
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) && netErr.Temporary() {
|
||||
slog.Warn("temporary accept error", "error", err)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("accept tls connection: %w", err)
|
||||
}
|
||||
|
||||
go handleConn(conn, fakeSite, sower, trojan)
|
||||
}
|
||||
}
|
||||
|
||||
func serve443(ln net.Listener, fakeSite string, sower *sower.Sower, trojan *trojan.Trojan) {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("serve 443 port")
|
||||
}
|
||||
go serve443(ln, fakeSite, sower, trojan)
|
||||
teeconn := teeconn.New(conn)
|
||||
defer teeconn.Close()
|
||||
func handleConn(conn net.Conn, fakeSite string, sower *transportSower.Sower, trojan *trojan.Trojan) {
|
||||
rereadConn := reread.New(conn)
|
||||
defer rereadConn.Close()
|
||||
|
||||
var addr net.Addr
|
||||
var dur time.Duration
|
||||
_ = rereadConn.SetReadDeadline(time.Now().Add(probeTimeout))
|
||||
|
||||
var (
|
||||
addr net.Addr
|
||||
dur time.Duration
|
||||
err error
|
||||
)
|
||||
defer func() {
|
||||
deferlog.DebugWarn(err).
|
||||
Dur("spend", dur).
|
||||
Msgf("relay conn to %s", addr)
|
||||
deferlog.DebugWarn(err, "relay conn", "took", dur, "addr", addr)
|
||||
}()
|
||||
|
||||
// 1. detect if it's a sower underlaying connection
|
||||
teeconn.Reread()
|
||||
if addr, err = sower.Unwrap(teeconn); err == nil {
|
||||
teeconn.Stop()
|
||||
|
||||
dur, err = relay.RelayTo(teeconn, addr.String())
|
||||
rereadConn.Reread()
|
||||
if addr, err = sower.Unwrap(rereadConn); err == nil {
|
||||
rereadConn.Stop()
|
||||
_ = rereadConn.SetReadDeadline(time.Time{})
|
||||
dur, err = relay.RelayTo(rereadConn, addr.String())
|
||||
return
|
||||
}
|
||||
|
||||
// 2. detect if it's a trojan underlaying connection
|
||||
teeconn.Reread()
|
||||
if addr, err = trojan.Unwrap(teeconn); err == nil {
|
||||
teeconn.Stop()
|
||||
|
||||
dur, err = relay.RelayTo(teeconn, addr.String())
|
||||
rereadConn.Reread()
|
||||
if addr, err = trojan.Unwrap(rereadConn); err == nil {
|
||||
rereadConn.Stop()
|
||||
_ = rereadConn.SetReadDeadline(time.Time{})
|
||||
dur, err = relay.RelayTo(rereadConn, addr.String())
|
||||
return
|
||||
}
|
||||
|
||||
// 3. fallback to fake site
|
||||
teeconn.Stop().Reread()
|
||||
dur, err = relay.RelayTo(teeconn, fakeSite)
|
||||
rereadConn.Stop().Reread()
|
||||
_ = rereadConn.SetReadDeadline(time.Time{})
|
||||
dur, err = relay.RelayTo(rereadConn, fakeSite)
|
||||
}
|
||||
|
||||
func sanitizeConfig(cfg config.SowerdConfig) map[string]any {
|
||||
return map[string]any{
|
||||
"log_level": cfg.LogLevel.String(),
|
||||
"serve_ip": cfg.ServeIP,
|
||||
"fake_site": cfg.FakeSite,
|
||||
"cert": map[string]any{
|
||||
"email": cfg.Cert.Email,
|
||||
"cert_config": cfg.Cert.Cert != "",
|
||||
"key_config": cfg.Cert.Key != "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func hasInstallFlag(args []string) bool {
|
||||
for _, arg := range args {
|
||||
switch arg {
|
||||
case "-i", "--install":
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func stdinConfirm(label string) bool {
|
||||
fmt.Printf("%s [y/N]: ", label)
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
if !scanner.Scan() {
|
||||
return false
|
||||
}
|
||||
answer := strings.TrimSpace(strings.ToLower(scanner.Text()))
|
||||
return answer == "y" || answer == "yes"
|
||||
}
|
||||
|
||||
func isLoopbackRemoteAddr(remoteAddr string) bool {
|
||||
host, _, err := net.SplitHostPort(remoteAddr)
|
||||
if err != nil {
|
||||
host = remoteAddr
|
||||
}
|
||||
host = strings.Trim(host, "[]")
|
||||
|
||||
ip := net.ParseIP(host)
|
||||
return ip != nil && ip.IsLoopback()
|
||||
}
|
||||
|
||||
func setLogger(level slog.Level) {
|
||||
fi, err := os.Stdout.Stat()
|
||||
noColor := err != nil || (fi.Mode()&os.ModeCharDevice) == 0
|
||||
deferlog.SetDefault(slog.New(tint.NewHandler(os.Stdout, &tint.Options{
|
||||
AddSource: true,
|
||||
NoColor: noColor,
|
||||
Level: level,
|
||||
})))
|
||||
}
|
||||
|
||||
func shutdownHTTP(ctx context.Context, server *http.Server) error {
|
||||
if err := server.Shutdown(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return fmt.Errorf("shutdown http server: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"github.com/sower-proxy/sower/config"
|
||||
)
|
||||
|
||||
func TestSanitizeConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := config.SowerdConfig{
|
||||
LogLevel: slog.LevelDebug,
|
||||
ServeIP: "0.0.0.0",
|
||||
Password: "secret",
|
||||
FakeSite: "127.0.0.1:8080",
|
||||
}
|
||||
cfg.Cert.Email = "ops@example.com"
|
||||
cfg.Cert.Cert = "/tmp/cert.pem"
|
||||
cfg.Cert.Key = "/tmp/key.pem"
|
||||
|
||||
got := sanitizeConfig(cfg)
|
||||
if got["fake_site"] != cfg.FakeSite {
|
||||
t.Fatalf("unexpected fake_site: %#v", got["fake_site"])
|
||||
}
|
||||
if _, ok := got["password"]; ok {
|
||||
t.Fatal("password must not be logged")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsLoopbackRemoteAddr(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
addr string
|
||||
want bool
|
||||
}{
|
||||
{addr: "127.0.0.1:1234", want: true},
|
||||
{addr: "[::1]:443", want: true},
|
||||
{addr: "10.0.0.1:1234", want: false},
|
||||
{addr: "not-an-addr", want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.addr, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := isLoopbackRemoteAddr(tt.addr); got != tt.want {
|
||||
t.Fatalf("isLoopbackRemoteAddr(%q) = %v, want %v", tt.addr, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasInstallFlag(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want bool
|
||||
}{
|
||||
{name: "short flag", args: []string{"-i"}, want: true},
|
||||
{name: "long flag", args: []string{"--install"}, want: true},
|
||||
{name: "missing flag", args: []string{"-c", "/etc/sower/sowerd.toml"}, want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := hasInstallFlag(tt.args); got != tt.want {
|
||||
t.Fatalf("hasInstallFlag(%q) = %v, want %v", tt.args, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/sower-proxy/sower/pkg/upstreamtls"
|
||||
)
|
||||
|
||||
type RemoteTLSConfig struct {
|
||||
ServerName string `usage:"override upstream TLS server name (SNI)"`
|
||||
ClientHello string `usage:"uTLS client hello: chrome, firefox, ios, android, edge, safari, 360, qq, randomized, randomized_alpn, randomized_no_alpn, golang"`
|
||||
InsecureSkipVerify bool `default:"false" usage:"skip upstream TLS certificate verification"`
|
||||
}
|
||||
|
||||
type RemoteConfig struct {
|
||||
Type string `default:"sower" required:"true" usage:"option: sower/trojan/socks5"`
|
||||
Addr string `required:"true" usage:"proxy address, eg: proxy.com or proxy.com:443"`
|
||||
Password string `usage:"remote proxy password"`
|
||||
TLS RemoteTLSConfig `flag:"tls"`
|
||||
}
|
||||
|
||||
// SowerConfig represents the configuration for sower client
|
||||
type SowerConfig struct {
|
||||
LogLevel slog.Level `default:"info" usage:"log level: debug, info, warn, error"`
|
||||
|
||||
Remote RemoteConfig
|
||||
|
||||
DNS struct {
|
||||
Disable bool `default:"false" usage:"disable DNS proxy"`
|
||||
Serve string `usage:"dns server ip"`
|
||||
Serve6 string `usage:"dns server ipv6, eg: ::1"`
|
||||
ServeIface string `usage:"use the IP in the net interface, if serve ip not setted. eg: eth0"`
|
||||
Upstream string `usage:"upstream dns server"`
|
||||
Fallback string `default:"223.5.5.5" required:"true" usage:"fallback dns server"`
|
||||
}
|
||||
Socks5 struct {
|
||||
Disable bool `default:"false" usage:"disable sock5 proxy"`
|
||||
Addr string `default:"127.0.0.1:1080" usage:"socks5 listen address"`
|
||||
} `flag:"socks5"`
|
||||
|
||||
Router struct {
|
||||
Block struct {
|
||||
File string `usage:"block list file, local file or remote"`
|
||||
FilePrefix string `default:"**." usage:"parsed as '<prefix>line_text'"`
|
||||
Rules []string `usage:"block list rules"`
|
||||
}
|
||||
Direct struct {
|
||||
File string `usage:"direct list file, local file or remote"`
|
||||
FilePrefix string `default:"**." usage:"parsed as '<prefix>line_text'"`
|
||||
Rules []string `usage:"direct list rules"`
|
||||
}
|
||||
Proxy struct {
|
||||
File string `usage:"proxy list file, local file or remote"`
|
||||
FilePrefix string `default:"**." usage:"parsed as '<prefix>line_text'"`
|
||||
Rules []string `usage:"proxy list rules"`
|
||||
}
|
||||
|
||||
Country struct {
|
||||
MMDB string `usage:"mmdb file"`
|
||||
File string `usage:"CIDR block list file, local file or remote"`
|
||||
FilePrefix string `default:"" usage:"parsed as '<prefix>line_text'"`
|
||||
Rules []string `usage:"CIDR list rules"`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate implements the validation interface for SowerConfig
|
||||
func (c *SowerConfig) Validate() error {
|
||||
switch c.Remote.Type {
|
||||
case "sower", "trojan", "socks5":
|
||||
default:
|
||||
return fmt.Errorf("unsupported remote type %q", c.Remote.Type)
|
||||
}
|
||||
|
||||
remoteHost, err := validateRemoteAddr(c.Remote.Type, c.Remote.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.Remote.TLS.ClientHello != "" {
|
||||
if err := upstreamtls.ValidateClientHello(c.Remote.TLS.ClientHello); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.DNS.ServeIface != "" {
|
||||
iface, err := net.InterfaceByName(c.DNS.ServeIface)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get interface: %w", err)
|
||||
}
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get interface addresses: %w", err)
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
ip, _, err := net.ParseCIDR(addr.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse interface IP: %w", err)
|
||||
}
|
||||
|
||||
if ip.To4() != nil { // ipv4
|
||||
if c.DNS.Serve == "" {
|
||||
c.DNS.Serve = ip.String()
|
||||
}
|
||||
} else if ip.IsGlobalUnicast() { // ipv6 must be global unicast
|
||||
if c.DNS.Serve6 == "" {
|
||||
c.DNS.Serve6 = ip.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateOptionalIP("dns serve", c.DNS.Serve); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateOptionalIP("dns serve6", c.DNS.Serve6); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateOptionalIP("dns upstream", c.DNS.Upstream); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateRequiredIP("dns fallback", c.DNS.Fallback); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !c.DNS.Disable && c.DNS.Serve == "" {
|
||||
return fmt.Errorf("dns serve ip and serve interface not set")
|
||||
}
|
||||
if !c.Socks5.Disable {
|
||||
if _, _, err := net.SplitHostPort(c.Socks5.Addr); err != nil {
|
||||
return fmt.Errorf("invalid socks5 listen address %q: %w", c.Socks5.Addr, err)
|
||||
}
|
||||
}
|
||||
|
||||
c.Router.Direct.Rules = append(c.Router.Direct.Rules,
|
||||
remoteHost, "**.in-addr.arpa", "**.ip6.arpa")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRemoteAddr(remoteType, addr string) (string, error) {
|
||||
switch remoteType {
|
||||
case "socks5":
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid remote socks5 address %q: %w", addr, err)
|
||||
}
|
||||
if host == "" {
|
||||
return "", fmt.Errorf("invalid remote socks5 address %q", addr)
|
||||
}
|
||||
return host, nil
|
||||
default:
|
||||
host, _, err := splitRemoteAddr(addr, "443")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid remote tls address %q: %w", addr, err)
|
||||
}
|
||||
return host, nil
|
||||
}
|
||||
}
|
||||
|
||||
func splitRemoteAddr(addr, defaultPort string) (string, string, error) {
|
||||
if addr == "" {
|
||||
return "", "", fmt.Errorf("empty address")
|
||||
}
|
||||
if host, port, err := net.SplitHostPort(addr); err == nil {
|
||||
if host == "" || port == "" {
|
||||
return "", "", fmt.Errorf("invalid address %q", addr)
|
||||
}
|
||||
return host, port, nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(addr, "[") || strings.HasSuffix(addr, "]") {
|
||||
return "", "", fmt.Errorf("invalid address %q", addr)
|
||||
}
|
||||
if strings.Count(addr, ":") == 1 {
|
||||
return "", "", fmt.Errorf("missing or invalid port in %q", addr)
|
||||
}
|
||||
return addr, defaultPort, nil
|
||||
}
|
||||
|
||||
func validateOptionalIP(name, value string) error {
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
return validateRequiredIP(name, value)
|
||||
}
|
||||
|
||||
func validateRequiredIP(name, value string) error {
|
||||
if net.ParseIP(value) == nil {
|
||||
return fmt.Errorf("invalid %s %q", name, value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
# Sower configuration example (TOML format)
|
||||
# This is the client configuration for the sower proxy
|
||||
|
||||
# Logging configuration
|
||||
logLevel = "info" # Log level: debug, info, warn, error
|
||||
|
||||
# Remote proxy configuration
|
||||
[remote]
|
||||
type = "sower" # Proxy type: sower, trojan, or socks5
|
||||
addr = "proxy.example.com" # Proxy server address, optional port for TLS remotes
|
||||
password = "your_secure_password" # Proxy password
|
||||
|
||||
[remote.tls]
|
||||
serverName = "" # Override upstream TLS server name (SNI)
|
||||
clientHello = "" # Optional uTLS fingerprint: chrome, firefox, ios, android, edge, safari, 360, qq, randomized, randomized_alpn, randomized_no_alpn, golang
|
||||
insecureSkipVerify = false # Skip upstream TLS certificate verification
|
||||
|
||||
# DNS configuration
|
||||
[dns]
|
||||
disable = false # Disable DNS proxy
|
||||
serve = "127.0.0.1" # DNS server IP address
|
||||
serve6 = "" # DNS server IPv6 address (optional)
|
||||
serveIface = "" # Network interface to get IP from (optional)
|
||||
upstream = "8.8.8.8" # Upstream DNS server (optional)
|
||||
fallback = "223.5.5.5" # Fallback DNS server
|
||||
|
||||
# SOCKS5 proxy configuration
|
||||
[socks5]
|
||||
disable = false # Disable SOCKS5 proxy
|
||||
addr = "127.0.0.1:1080" # SOCKS5 listen address
|
||||
|
||||
# Router configuration
|
||||
[router]
|
||||
# Block list rules
|
||||
[router.block]
|
||||
file = "" # Block list file path (local or remote)
|
||||
filePrefix = "**." # Prefix for block list rules
|
||||
rules = [] # Additional block rules
|
||||
|
||||
# Direct list rules
|
||||
[router.direct]
|
||||
file = "" # Direct list file path (local or remote)
|
||||
filePrefix = "**." # Prefix for direct list rules
|
||||
rules = [] # Additional direct rules
|
||||
|
||||
# Proxy list rules
|
||||
[router.proxy]
|
||||
file = "" # Proxy list file path (local or remote)
|
||||
filePrefix = "**." # Prefix for proxy list rules
|
||||
rules = [] # Additional proxy rules
|
||||
|
||||
# Country-based routing
|
||||
[router.country]
|
||||
mmdb = "" # MMDB file path for GeoIP
|
||||
file = "" # CIDR block list file path
|
||||
filePrefix = "" # Prefix for CIDR rules
|
||||
rules = [] # Additional CIDR rules
|
||||
@@ -0,0 +1,56 @@
|
||||
# Sower configuration example (YAML format)
|
||||
# This is the client configuration for the sower proxy
|
||||
|
||||
# Logging configuration
|
||||
logLevel: "info" # Log level: debug, info, warn, error
|
||||
|
||||
# Remote proxy configuration
|
||||
remote:
|
||||
type: "sower" # Proxy type: sower, trojan, or socks5
|
||||
addr: "proxy.example.com" # Proxy server address, optional port for TLS remotes
|
||||
password: "your_secure_password" # Proxy password
|
||||
tls:
|
||||
serverName: "" # Override upstream TLS server name (SNI)
|
||||
clientHello: "" # Optional uTLS fingerprint: chrome, firefox, ios, android, edge, safari, 360, qq, randomized, randomized_alpn, randomized_no_alpn, golang
|
||||
insecureSkipVerify: false # Skip upstream TLS certificate verification
|
||||
|
||||
# DNS configuration
|
||||
dns:
|
||||
disable: false # Disable DNS proxy
|
||||
serve: "127.0.0.1" # DNS server IP address
|
||||
serve6: "" # DNS server IPv6 address (optional)
|
||||
serveIface: "" # Network interface to get IP from (optional)
|
||||
upstream: "8.8.8.8" # Upstream DNS server (optional)
|
||||
fallback: "223.5.5.5" # Fallback DNS server
|
||||
|
||||
# SOCKS5 proxy configuration
|
||||
socks5:
|
||||
disable: false # Disable SOCKS5 proxy
|
||||
addr: "127.0.0.1:1080" # SOCKS5 listen address
|
||||
|
||||
# Router configuration
|
||||
router:
|
||||
# Block list rules
|
||||
block:
|
||||
file: "" # Block list file path (local or remote)
|
||||
filePrefix: "**." # Prefix for block list rules
|
||||
rules: [] # Additional block rules
|
||||
|
||||
# Direct list rules
|
||||
direct:
|
||||
file: "" # Direct list file path (local or remote)
|
||||
filePrefix: "**." # Prefix for direct list rules
|
||||
rules: [] # Additional direct rules
|
||||
|
||||
# Proxy list rules
|
||||
proxy:
|
||||
file: "" # Proxy list file path (local or remote)
|
||||
filePrefix: "**." # Prefix for proxy list rules
|
||||
rules: [] # Additional proxy rules
|
||||
|
||||
# Country-based routing
|
||||
country:
|
||||
mmdb: "" # MMDB file path for GeoIP
|
||||
file: "" # CIDR block list file path
|
||||
filePrefix: "" # Prefix for CIDR rules
|
||||
rules: [] # Additional CIDR rules
|
||||
@@ -0,0 +1,79 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSowerConfigValidateRejectsInvalidRemoteType(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := SowerConfig{}
|
||||
cfg.Remote.Type = "invalid"
|
||||
cfg.Remote.Addr = "example.com"
|
||||
cfg.DNS.Disable = true
|
||||
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Fatal("expected validation error for invalid remote type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSowerConfigValidateRejectsInvalidSocks5Addr(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := SowerConfig{}
|
||||
cfg.Remote.Type = "sower"
|
||||
cfg.Remote.Addr = "example.com"
|
||||
cfg.DNS.Disable = true
|
||||
cfg.Socks5.Addr = "127.0.0.1"
|
||||
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Fatal("expected validation error for invalid socks5 address")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSowerConfigValidateRejectsInvalidFallbackIP(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := SowerConfig{}
|
||||
cfg.Remote.Type = "sower"
|
||||
cfg.Remote.Addr = "example.com"
|
||||
cfg.DNS.Disable = true
|
||||
cfg.DNS.Fallback = "not-an-ip"
|
||||
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Fatal("expected validation error for invalid fallback IP")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSowerConfigValidateAllowsTLSRemoteWithExplicitPort(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := SowerConfig{}
|
||||
cfg.Remote.Type = "sower"
|
||||
cfg.Remote.Addr = "example.com:8443"
|
||||
cfg.Remote.TLS.ClientHello = "chrome"
|
||||
cfg.DNS.Disable = true
|
||||
cfg.DNS.Fallback = "223.5.5.5"
|
||||
cfg.Socks5.Disable = true
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("validate config: %v", err)
|
||||
}
|
||||
if got := cfg.Router.Direct.Rules[len(cfg.Router.Direct.Rules)-3]; got != "example.com" {
|
||||
t.Fatalf("unexpected direct rule host: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSowerConfigValidateRejectsInvalidTLSClientHello(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := SowerConfig{}
|
||||
cfg.Remote.Type = "trojan"
|
||||
cfg.Remote.Addr = "example.com"
|
||||
cfg.Remote.TLS.ClientHello = "not-supported"
|
||||
cfg.DNS.Disable = true
|
||||
cfg.DNS.Fallback = "223.5.5.5"
|
||||
cfg.Socks5.Disable = true
|
||||
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Fatal("expected validation error for invalid TLS client hello")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"os"
|
||||
)
|
||||
|
||||
const ExampleSowerdConfigTOML = `# Sowerd configuration example (TOML format)
|
||||
# This is the daemon configuration for the sower server
|
||||
|
||||
# Logging configuration
|
||||
logLevel = "info" # Log level: debug, info, warn, error
|
||||
|
||||
# Server configuration
|
||||
serveIP = "0.0.0.0" # IP address to listen on for ports 80 and 443
|
||||
password = "change_me" # Password for client authentication
|
||||
fakeSite = "/var/www" # Fake site directory or address (default: /var/www)
|
||||
|
||||
# SSL/TLS certificate configuration
|
||||
[cert]
|
||||
email = "your-email@example.com" # Email for Let's Encrypt certificate
|
||||
cert = "" # Path to custom certificate file (optional)
|
||||
key = "" # Path to custom private key file (optional)
|
||||
`
|
||||
|
||||
// SowerdConfig represents the configuration for sowerd daemon
|
||||
type SowerdConfig struct {
|
||||
LogLevel slog.Level `default:"info" usage:"log level: debug, info, warn, error"`
|
||||
ServeIP string `usage:"listen to port 80 443 of the IP"`
|
||||
Password string `required:"true"`
|
||||
FakeSite string `default:"/var/www" usage:"fake site address or directoy. serving on 127.0.0.1:80 if directory"`
|
||||
|
||||
Cert struct {
|
||||
Email string
|
||||
Cert string
|
||||
Key string
|
||||
}
|
||||
}
|
||||
|
||||
// Validate implements the validation interface for SowerdConfig
|
||||
func (c *SowerdConfig) Validate() error {
|
||||
if c.ServeIP != "" && net.ParseIP(c.ServeIP) == nil {
|
||||
return fmt.Errorf("invalid serve ip: %q", c.ServeIP)
|
||||
}
|
||||
if c.Password == "" {
|
||||
return fmt.Errorf("password is required")
|
||||
}
|
||||
if c.FakeSite == "" {
|
||||
return fmt.Errorf("fake site is required")
|
||||
}
|
||||
|
||||
if (c.Cert.Cert == "") != (c.Cert.Key == "") {
|
||||
return fmt.Errorf("cert and key must be configured together")
|
||||
}
|
||||
|
||||
if c.Cert.Cert != "" {
|
||||
if _, err := os.Stat(c.Cert.Cert); err != nil {
|
||||
return fmt.Errorf("stat cert file: %w", err)
|
||||
}
|
||||
if _, err := os.Stat(c.Cert.Key); err != nil {
|
||||
return fmt.Errorf("stat key file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(c.FakeSite); err == nil {
|
||||
return nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("stat fake site: %w", err)
|
||||
}
|
||||
|
||||
if _, _, err := net.SplitHostPort(c.FakeSite); err != nil {
|
||||
return fmt.Errorf("fake site must be an existing directory or host:port: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
# Sowerd configuration example (TOML format)
|
||||
# This is the daemon configuration for the sower server
|
||||
|
||||
# Logging configuration
|
||||
logLevel = "info" # Log level: debug, info, warn, error
|
||||
|
||||
# Server configuration
|
||||
serveIP = "0.0.0.0" # IP address to listen on for ports 80 and 443
|
||||
password = "change_me" # Password for client authentication
|
||||
fakeSite = "/var/www" # Fake site directory or address (default: /var/www)
|
||||
|
||||
# SSL/TLS certificate configuration
|
||||
[cert]
|
||||
email = "your-email@example.com" # Email for Let's Encrypt certificate
|
||||
cert = "" # Path to custom certificate file (optional)
|
||||
key = "" # Path to custom private key file (optional)
|
||||
@@ -0,0 +1,16 @@
|
||||
# Sowerd configuration example (YAML format)
|
||||
# This is the daemon configuration for the sower server
|
||||
|
||||
# Logging configuration
|
||||
logLevel: "info" # Log level: debug, info, warn, error
|
||||
|
||||
# Server configuration
|
||||
serveIP: "0.0.0.0" # IP address to listen on for ports 80 and 443
|
||||
password: "change_me" # Password for client authentication
|
||||
fakeSite: "/var/www" # Fake site directory or address (default: /var/www)
|
||||
|
||||
# SSL/TLS certificate configuration
|
||||
cert:
|
||||
email: "your-email@example.com" # Email for Let's Encrypt certificate
|
||||
cert: "" # Path to custom certificate file (optional)
|
||||
key: "" # Path to custom private key file (optional)
|
||||
@@ -0,0 +1,76 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSowerdConfigValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg SowerdConfig
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid remote fake site",
|
||||
cfg: SowerdConfig{
|
||||
ServeIP: "0.0.0.0",
|
||||
Password: "secret",
|
||||
FakeSite: "127.0.0.1:8080",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid serve ip",
|
||||
cfg: SowerdConfig{
|
||||
ServeIP: "bad-ip",
|
||||
Password: "secret",
|
||||
FakeSite: "127.0.0.1:8080",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "missing password",
|
||||
cfg: SowerdConfig{
|
||||
FakeSite: "127.0.0.1:8080",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid fake site address",
|
||||
cfg: SowerdConfig{
|
||||
Password: "secret",
|
||||
FakeSite: "not-an-address",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "partial cert config",
|
||||
cfg: SowerdConfig{
|
||||
Password: "secret",
|
||||
FakeSite: "127.0.0.1:8080",
|
||||
Cert: struct {
|
||||
Email string
|
||||
Cert string
|
||||
Key string
|
||||
}{
|
||||
Cert: "/tmp/cert.pem",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := tt.cfg.Validate()
|
||||
if tt.wantErr && err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !tt.wantErr && err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,37 @@
|
||||
module github.com/wweir/sower
|
||||
module github.com/sower-proxy/sower
|
||||
|
||||
go 1.18
|
||||
go 1.26.1
|
||||
|
||||
require (
|
||||
github.com/cristalhq/aconfig v0.17.0
|
||||
github.com/cristalhq/aconfig/aconfighcl v0.16.1
|
||||
github.com/cristalhq/aconfig/aconfigtoml v0.16.1
|
||||
github.com/cristalhq/aconfig/aconfigyaml v0.17.0
|
||||
github.com/cristalhq/aconfig v0.19.0
|
||||
github.com/cristalhq/aconfig/aconfighcl v0.17.1
|
||||
github.com/cristalhq/aconfig/aconfigtoml v0.17.1
|
||||
github.com/cristalhq/aconfig/aconfigyaml v0.17.1
|
||||
github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771
|
||||
github.com/libp2p/go-reuseport v0.2.0
|
||||
github.com/miekg/dns v1.1.49
|
||||
github.com/oschwald/geoip2-golang v1.7.0
|
||||
github.com/libp2p/go-reuseport v0.4.0
|
||||
github.com/lmittmann/tint v1.1.3
|
||||
github.com/miekg/dns v1.1.72
|
||||
github.com/oschwald/geoip2-golang v1.13.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/sower-proxy/conns v0.0.3
|
||||
github.com/sower-proxy/deferlog v1.0.4
|
||||
github.com/refraction-networking/utls v1.8.2
|
||||
github.com/sower-proxy/conns v0.1.1
|
||||
github.com/sower-proxy/deferlog/v2 v2.1.4
|
||||
github.com/sower-proxy/mem v0.0.3
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e
|
||||
golang.org/x/crypto v0.49.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/oschwald/maxminddb-golang v1.9.0 // indirect
|
||||
github.com/rs/zerolog v1.26.1 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
|
||||
golang.org/x/net v0.0.0-20220531201128-c960675eff93 // indirect
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/tools v0.1.10 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,92 +1,66 @@
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
|
||||
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cristalhq/aconfig v0.16.1/go.mod h1:NXaRp+1e6bkO4dJn+wZ71xyaihMDYPtCSvEhMTm/H3E=
|
||||
github.com/cristalhq/aconfig v0.17.0 h1:VYqg0YOM5yUEx0KH/VwUYF2e/PNI7dcUE66y+xEx73s=
|
||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/cristalhq/aconfig v0.17.0/go.mod h1:NXaRp+1e6bkO4dJn+wZ71xyaihMDYPtCSvEhMTm/H3E=
|
||||
github.com/cristalhq/aconfig/aconfighcl v0.16.1 h1:RiziovFKOwB6+H1LCD7DXxrFgoKDEJxxYLSsAOsj+vQ=
|
||||
github.com/cristalhq/aconfig/aconfighcl v0.16.1/go.mod h1:YvZyVGtty/7yIdyOPPl68u7ENaUMq6ybZAph+n3g4II=
|
||||
github.com/cristalhq/aconfig/aconfigtoml v0.16.1 h1:q9DJfJqdLvZj7vL3bOIR6QM0ySpA0i6X8CZ8FNob6nc=
|
||||
github.com/cristalhq/aconfig/aconfigtoml v0.16.1/go.mod h1:VfRDnJBq09TZXiQdnnd4CBsNyj4s7AEDJlCxDSDt5DI=
|
||||
github.com/cristalhq/aconfig/aconfigyaml v0.17.0 h1:71HHzscq9qmuNPhwSXy+852EFQ5c+X1mGakKtDvsRd4=
|
||||
github.com/cristalhq/aconfig/aconfigyaml v0.17.0/go.mod h1:xKup1zPhArI0Ft/UP5uldQMZcT7TylJPEtilnvy+6Zs=
|
||||
github.com/cristalhq/aconfig v0.19.0 h1:fAo9ZObtzboHnf+5eAoMfb9KTDU5G/ij8OYO2wbpmM0=
|
||||
github.com/cristalhq/aconfig v0.19.0/go.mod h1:9ogrGEt9yU5V4pif/ThkVUfhj8JkdV+iDeahZGgfnDU=
|
||||
github.com/cristalhq/aconfig/aconfighcl v0.17.1 h1:/hJvmmoP3akvjY8qARiXgN30m/WXeBvpQdQ8v4DE8W8=
|
||||
github.com/cristalhq/aconfig/aconfighcl v0.17.1/go.mod h1:oOPqMmdUjVW6pKKO1zkOwQKHSUD0rlu8F5do+pylOQc=
|
||||
github.com/cristalhq/aconfig/aconfigtoml v0.17.1 h1:TA3xH8mALD8YeULsr4v87cMbKGBma35lWeRga4Xn11Q=
|
||||
github.com/cristalhq/aconfig/aconfigtoml v0.17.1/go.mod h1:xt4kCEjhgvHWO/oDGJHQHrW5CnvSEoBjJhRXVBdYbhQ=
|
||||
github.com/cristalhq/aconfig/aconfigyaml v0.17.1 h1:xCCbRKVmKrft9gQj3gHOq6U5PduasvlXEIsxtyzmFZ0=
|
||||
github.com/cristalhq/aconfig/aconfigyaml v0.17.1/go.mod h1:5DTsjHkvQ6hfbyxfG32roB1lF0U82rROtFaLxibL8V8=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771 h1:t2c2B9g1ZVhMYduqmANSEGVD3/1WlsrEYNPtVoFlENk=
|
||||
github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771/go.mod h1:0AqAH3ZogsCrvrtUpvc6EtVKbc3w6xwZhkvGLuqyi3o=
|
||||
github.com/libp2p/go-reuseport v0.2.0 h1:18PRvIMlpY6ZK85nIAicSBuXXvrYoSw3dsBAR7zc560=
|
||||
github.com/libp2p/go-reuseport v0.2.0/go.mod h1:bvVho6eLMm6Bz5hmU0LYN3ixd3nPPvtIlaURZZgOY4k=
|
||||
github.com/miekg/dns v1.1.49 h1:qe0mQU3Z/XpFeE+AEBo2rqaS1IPBJ3anmqZ4XiZJVG8=
|
||||
github.com/miekg/dns v1.1.49/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
||||
github.com/oschwald/geoip2-golang v1.7.0 h1:JW1r5AKi+vv2ujSxjKthySK3jo8w8oKWPyXsw+Qs/S8=
|
||||
github.com/oschwald/geoip2-golang v1.7.0/go.mod h1:mdI/C7iK7NVMcIDDtf4bCKMJ7r0o7UwGeCo9eiitCMQ=
|
||||
github.com/oschwald/maxminddb-golang v1.9.0 h1:tIk4nv6VT9OiPyrnDAfJS1s1xKDQMZOsGojab6EjC1Y=
|
||||
github.com/oschwald/maxminddb-golang v1.9.0/go.mod h1:TK+s/Z2oZq0rSl4PSeAEoP0bgm82Cp5HyvYbt8K3zLY=
|
||||
github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s=
|
||||
github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU=
|
||||
github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I=
|
||||
github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI=
|
||||
github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
|
||||
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
|
||||
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
|
||||
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
|
||||
github.com/sower-proxy/conns v0.0.3 h1:3jbkvPhDwC+ph/NZi7Gywks9WakHK4ept7CkjTiNUJ8=
|
||||
github.com/sower-proxy/conns v0.0.3/go.mod h1:p3ZM/8MolzW4TN0LaiPJnwnhhif3y9sBi2vQEWHvwhg=
|
||||
github.com/sower-proxy/deferlog v1.0.4 h1:ipaWRvEel+/nyRD6/tRRSJpMKeM6HgtD/VsIUIWveaQ=
|
||||
github.com/sower-proxy/deferlog v1.0.4/go.mod h1:PckF0JOjkPQj00StdPXGXCO7ys8LoW/biHxt3NunNaQ=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
|
||||
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/sower-proxy/conns v0.1.1 h1:9VPVW+sFc/y/IvXbUQTbXNSPF3I+1IG5mbrQTt5bdTE=
|
||||
github.com/sower-proxy/conns v0.1.1/go.mod h1:p3ZM/8MolzW4TN0LaiPJnwnhhif3y9sBi2vQEWHvwhg=
|
||||
github.com/sower-proxy/deferlog/v2 v2.1.4 h1:Vo/iVWGw3Su625RP2RIy4mE5FlfRdfrgP7kwOWNOyec=
|
||||
github.com/sower-proxy/deferlog/v2 v2.1.4/go.mod h1:mdLrL/bxiTNhxeRLtRgKAecQddugI0iJ0ArC3yDwVSw=
|
||||
github.com/sower-proxy/mem v0.0.3 h1:JK86etQ2QxRUXhs9JhoKYz5p3pFBSrI3UXVhwm44OzM=
|
||||
github.com/sower-proxy/mem v0.0.3/go.mod h1:tM3P+gCAZMVBnOixGVAHS34si9Iwj2HU6M4IVEv4rK0=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220531201128-c960675eff93 h1:MYimHLfoXEpOhqd/zgoA/uoXzHB86AEky4LAx5ij9xA=
|
||||
golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618=
|
||||
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sower-proxy/sower/config"
|
||||
)
|
||||
|
||||
type ConfirmFunc func(label string) bool
|
||||
|
||||
type installOptions struct {
|
||||
confirm ConfirmFunc
|
||||
geteuid func() int
|
||||
executable func() (string, error)
|
||||
evalSymlinks func(string) (string, error)
|
||||
serviceIsActive func() bool
|
||||
systemctl func(args ...string) error
|
||||
targetBinaryPath string
|
||||
servicePath string
|
||||
configPath string
|
||||
defaultFakeSite string
|
||||
defaultConfigTOML string
|
||||
}
|
||||
|
||||
func InstallService(confirm ConfirmFunc) error {
|
||||
return installService(installOptions{
|
||||
confirm: confirm,
|
||||
geteuid: os.Geteuid,
|
||||
executable: os.Executable,
|
||||
evalSymlinks: filepath.EvalSymlinks,
|
||||
serviceIsActive: serviceIsActive,
|
||||
systemctl: systemctl,
|
||||
targetBinaryPath: "/usr/local/bin/sowerd",
|
||||
servicePath: "/etc/systemd/system/sowerd.service",
|
||||
configPath: "/etc/sower/sowerd.toml",
|
||||
defaultFakeSite: "/var/www",
|
||||
defaultConfigTOML: config.ExampleSowerdConfigTOML,
|
||||
})
|
||||
}
|
||||
|
||||
func installService(opts installOptions) error {
|
||||
if opts.geteuid() != 0 {
|
||||
return fmt.Errorf("installing systemd service requires root privileges, try running with sudo")
|
||||
}
|
||||
|
||||
execPath, err := opts.executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get executable path: %w", err)
|
||||
}
|
||||
execPath, err = opts.evalSymlinks(execPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve executable symlink: %w", err)
|
||||
}
|
||||
|
||||
isUpdate := opts.serviceIsActive()
|
||||
if execPath != opts.targetBinaryPath {
|
||||
label := fmt.Sprintf("Copy binary to %s?", opts.targetBinaryPath)
|
||||
if isUpdate {
|
||||
label = fmt.Sprintf("Update binary at %s?", opts.targetBinaryPath)
|
||||
}
|
||||
if opts.confirm != nil && opts.confirm(label) {
|
||||
if err := copyFile(execPath, opts.targetBinaryPath, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if isUpdate {
|
||||
_ = opts.systemctl("stop", "sowerd")
|
||||
}
|
||||
fmt.Printf(" OK binary copied to %s\n", opts.targetBinaryPath)
|
||||
execPath = opts.targetBinaryPath
|
||||
}
|
||||
}
|
||||
|
||||
if err := writeServiceFile(opts.servicePath, execPath, opts.configPath); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf(" OK service file installed: %s\n", opts.servicePath)
|
||||
|
||||
if err := ensureDefaultFiles(opts.configPath, opts.defaultFakeSite, opts.defaultConfigTOML); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := opts.systemctl("daemon-reload"); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(" OK systemd daemon reloaded")
|
||||
|
||||
if isUpdate {
|
||||
return finishUpdate(opts.confirm, opts.systemctl)
|
||||
}
|
||||
return finishInstall(opts.confirm, opts.systemctl)
|
||||
}
|
||||
|
||||
func finishInstall(confirm ConfirmFunc, systemctlFn func(args ...string) error) error {
|
||||
if confirm == nil || !confirm("Start and enable sowerd service now?") {
|
||||
fmt.Println()
|
||||
fmt.Println("sowerd service installed. Next steps:")
|
||||
fmt.Println(" 1. Edit /etc/sower/sowerd.toml")
|
||||
fmt.Println(" 2. sudo systemctl start sowerd")
|
||||
fmt.Println(" 3. sudo systemctl enable sowerd")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := systemctlFn("enable", "sowerd"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := systemctlFn("start", "sowerd"); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(" OK service enabled and started")
|
||||
return nil
|
||||
}
|
||||
|
||||
func finishUpdate(confirm ConfirmFunc, systemctlFn func(args ...string) error) error {
|
||||
if confirm == nil || !confirm("Restart sowerd service now?") {
|
||||
fmt.Println()
|
||||
fmt.Println("sowerd service updated. To apply:")
|
||||
fmt.Println(" sudo systemctl restart sowerd")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := systemctlFn("restart", "sowerd"); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(" OK service restarted")
|
||||
return nil
|
||||
}
|
||||
|
||||
func serviceIsActive() bool {
|
||||
cmd := exec.Command("systemctl", "is-active", "--quiet", "sowerd")
|
||||
return cmd.Run() == nil
|
||||
}
|
||||
|
||||
func systemctl(args ...string) error {
|
||||
cmd := exec.Command("systemctl", args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("systemctl %s: %w - %s", strings.Join(args, " "), err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFile(src, dst string, mode os.FileMode) error {
|
||||
data, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read binary %s: %w", src, err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||
return fmt.Errorf("create binary directory: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(dst, data, mode); err != nil {
|
||||
return fmt.Errorf("write binary to %s: %w", dst, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeServiceFile(servicePath, execPath, configPath string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(servicePath), 0o755); err != nil {
|
||||
return fmt.Errorf("create service directory: %w", err)
|
||||
}
|
||||
|
||||
serviceContent := fmt.Sprintf(`[Unit]
|
||||
Description=sower server service
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=%s -c %s
|
||||
Restart=always
|
||||
RestartSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`, execPath, configPath)
|
||||
|
||||
if err := os.WriteFile(servicePath, []byte(serviceContent), 0o644); err != nil {
|
||||
return fmt.Errorf("write service file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureDefaultFiles(configPath, fakeSiteDir, defaultConfigTOML string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
|
||||
return fmt.Errorf("create config directory: %w", err)
|
||||
}
|
||||
if err := os.MkdirAll(fakeSiteDir, 0o755); err != nil {
|
||||
return fmt.Errorf("create fake site directory: %w", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
return nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("stat default config path: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, []byte(defaultConfigTOML), 0o644); err != nil {
|
||||
return fmt.Errorf("write default config: %w", err)
|
||||
}
|
||||
fmt.Printf(" OK default config written to %s\n", configPath)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInstallServiceRequiresRoot(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := installService(installOptions{
|
||||
geteuid: func() int { return 1000 },
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "root privileges") {
|
||||
t.Fatalf("installService() error = %v, want root privileges error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallServiceFreshInstall(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
execPath := filepath.Join(tmpDir, "sowerd")
|
||||
if err := os.WriteFile(execPath, []byte("binary"), 0o755); err != nil {
|
||||
t.Fatalf("write executable: %v", err)
|
||||
}
|
||||
|
||||
targetBinaryPath := filepath.Join(tmpDir, "usr", "local", "bin", "sowerd")
|
||||
servicePath := filepath.Join(tmpDir, "etc", "systemd", "system", "sowerd.service")
|
||||
configPath := filepath.Join(tmpDir, "etc", "sower", "sowerd.toml")
|
||||
fakeSiteDir := filepath.Join(tmpDir, "var", "www")
|
||||
|
||||
var prompts []string
|
||||
var systemctlCalls []string
|
||||
confirmAnswers := map[string]bool{
|
||||
"Copy binary to " + targetBinaryPath + "?": true,
|
||||
"Start and enable sowerd service now?": true,
|
||||
}
|
||||
|
||||
err := installService(installOptions{
|
||||
confirm: func(label string) bool {
|
||||
prompts = append(prompts, label)
|
||||
return confirmAnswers[label]
|
||||
},
|
||||
geteuid: func() int { return 0 },
|
||||
executable: func() (string, error) { return execPath, nil },
|
||||
evalSymlinks: func(path string) (string, error) { return path, nil },
|
||||
serviceIsActive: func() bool { return false },
|
||||
systemctl: func(args ...string) error {
|
||||
systemctlCalls = append(systemctlCalls, strings.Join(args, " "))
|
||||
return nil
|
||||
},
|
||||
targetBinaryPath: targetBinaryPath,
|
||||
servicePath: servicePath,
|
||||
configPath: configPath,
|
||||
defaultFakeSite: fakeSiteDir,
|
||||
defaultConfigTOML: `password = "change_me"` + "\n" + `fakeSite = "/var/www"` + "\n",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("installService() error = %v", err)
|
||||
}
|
||||
|
||||
gotBinary, err := os.ReadFile(targetBinaryPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read target binary: %v", err)
|
||||
}
|
||||
if string(gotBinary) != "binary" {
|
||||
t.Fatalf("target binary = %q, want %q", string(gotBinary), "binary")
|
||||
}
|
||||
|
||||
serviceContent, err := os.ReadFile(servicePath)
|
||||
if err != nil {
|
||||
t.Fatalf("read service file: %v", err)
|
||||
}
|
||||
serviceText := string(serviceContent)
|
||||
if !strings.Contains(serviceText, "ExecStart="+targetBinaryPath+" -c "+configPath) {
|
||||
t.Fatalf("service file missing ExecStart, content = %s", serviceText)
|
||||
}
|
||||
|
||||
configContent, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read config file: %v", err)
|
||||
}
|
||||
if string(configContent) == "" {
|
||||
t.Fatal("config file is empty")
|
||||
}
|
||||
|
||||
if info, err := os.Stat(fakeSiteDir); err != nil || !info.IsDir() {
|
||||
t.Fatalf("fake site dir stat err = %v, isDir = %v", err, err == nil && info.IsDir())
|
||||
}
|
||||
|
||||
wantPrompts := []string{
|
||||
"Copy binary to " + targetBinaryPath + "?",
|
||||
"Start and enable sowerd service now?",
|
||||
}
|
||||
if !reflect.DeepEqual(prompts, wantPrompts) {
|
||||
t.Fatalf("prompts = %#v, want %#v", prompts, wantPrompts)
|
||||
}
|
||||
|
||||
wantSystemctlCalls := []string{
|
||||
"daemon-reload",
|
||||
"enable sowerd",
|
||||
"start sowerd",
|
||||
}
|
||||
if !reflect.DeepEqual(systemctlCalls, wantSystemctlCalls) {
|
||||
t.Fatalf("systemctl calls = %#v, want %#v", systemctlCalls, wantSystemctlCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallServiceUpdateWithoutRestart(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
targetBinaryPath := filepath.Join(tmpDir, "usr", "local", "bin", "sowerd")
|
||||
if err := os.MkdirAll(filepath.Dir(targetBinaryPath), 0o755); err != nil {
|
||||
t.Fatalf("mkdir target binary dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(targetBinaryPath, []byte("binary"), 0o755); err != nil {
|
||||
t.Fatalf("write target binary: %v", err)
|
||||
}
|
||||
|
||||
servicePath := filepath.Join(tmpDir, "etc", "systemd", "system", "sowerd.service")
|
||||
configPath := filepath.Join(tmpDir, "etc", "sower", "sowerd.toml")
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
|
||||
t.Fatalf("mkdir config dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(configPath, []byte("password = \"existing\"\n"), 0o644); err != nil {
|
||||
t.Fatalf("write existing config: %v", err)
|
||||
}
|
||||
|
||||
var prompts []string
|
||||
var systemctlCalls []string
|
||||
|
||||
err := installService(installOptions{
|
||||
confirm: func(label string) bool {
|
||||
prompts = append(prompts, label)
|
||||
return false
|
||||
},
|
||||
geteuid: func() int { return 0 },
|
||||
executable: func() (string, error) { return targetBinaryPath, nil },
|
||||
evalSymlinks: func(path string) (string, error) { return path, nil },
|
||||
serviceIsActive: func() bool { return true },
|
||||
systemctl: func(args ...string) error {
|
||||
systemctlCalls = append(systemctlCalls, strings.Join(args, " "))
|
||||
return nil
|
||||
},
|
||||
targetBinaryPath: targetBinaryPath,
|
||||
servicePath: servicePath,
|
||||
configPath: configPath,
|
||||
defaultFakeSite: filepath.Join(tmpDir, "var", "www"),
|
||||
defaultConfigTOML: "password = \"change_me\"\n",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("installService() error = %v", err)
|
||||
}
|
||||
|
||||
configContent, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read config file: %v", err)
|
||||
}
|
||||
if string(configContent) != "password = \"existing\"\n" {
|
||||
t.Fatalf("config content = %q, want existing config to be preserved", string(configContent))
|
||||
}
|
||||
|
||||
wantPrompts := []string{"Restart sowerd service now?"}
|
||||
if !reflect.DeepEqual(prompts, wantPrompts) {
|
||||
t.Fatalf("prompts = %#v, want %#v", prompts, wantPrompts)
|
||||
}
|
||||
|
||||
wantSystemctlCalls := []string{"daemon-reload"}
|
||||
if !reflect.DeepEqual(systemctlCalls, wantSystemctlCalls) {
|
||||
t.Fatalf("systemctl calls = %#v, want %#v", systemctlCalls, wantSystemctlCalls)
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -1,7 +1,7 @@
|
||||
package dhcp
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"crypto/rand"
|
||||
"net"
|
||||
"runtime"
|
||||
"time"
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var xid = make([]byte, 4)
|
||||
var broadcastAddr, _ = net.ResolveUDPAddr("udp", "255.255.255.255:67")
|
||||
|
||||
func GetDNSServer() ([]string, error) {
|
||||
@@ -20,7 +19,10 @@ func GetDNSServer() ([]string, error) {
|
||||
return nil, errors.Wrap(err, "pick interface")
|
||||
}
|
||||
|
||||
rand.Read(xid)
|
||||
xid := make([]byte, 4)
|
||||
if _, err := rand.Read(xid); err != nil {
|
||||
return nil, errors.Wrap(err, "generate xid")
|
||||
}
|
||||
pack := dhcp4.RequestPacket(dhcp4.Discover, iface.HardwareAddr, net.IPv4(0, 0, 0, 0), xid, true, []dhcp4.Option{
|
||||
{Code: dhcp4.OptionRequestedIPAddress, Value: []byte(iface.IP.To4())},
|
||||
{Code: dhcp4.End},
|
||||
|
||||
@@ -3,7 +3,7 @@ package dhcp_test
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/wweir/sower/pkg/dhcp"
|
||||
"github.com/sower-proxy/sower/pkg/dhcp"
|
||||
)
|
||||
|
||||
func Example_dns() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package dhcp
|
||||
@@ -15,13 +16,23 @@ func PickInternetInterface() (*Iface, error) {
|
||||
}
|
||||
|
||||
for i := range ifaces {
|
||||
if ifaces[i].Flags&net.FlagUp == 0 || ifaces[i].Flags&net.FlagLoopback != 0 {
|
||||
continue
|
||||
}
|
||||
if len(ifaces[i].HardwareAddr) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
addrs, _ := ifaces[i].Addrs()
|
||||
addrs, err := ifaces[i].Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if ip := addr.(*net.IPNet).IP.To4(); ip != nil {
|
||||
ipNet, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if ip := ipNet.IP.To4(); ip != nil && ip.IsGlobalUnicast() {
|
||||
return &Iface{ifaces[i].HardwareAddr, ip}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package dhcp_test
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/wweir/sower/pkg/dhcp"
|
||||
"github.com/sower-proxy/sower/pkg/dhcp"
|
||||
)
|
||||
|
||||
func Example_iface() {
|
||||
|
||||
@@ -12,6 +12,7 @@ type Node struct {
|
||||
type node struct {
|
||||
secs []string
|
||||
subNodes []*node
|
||||
indexMap map[string]int
|
||||
}
|
||||
|
||||
func NewNodeFromRules(rules ...string) *Node {
|
||||
@@ -50,7 +51,7 @@ func (n *node) string(prefix, indent string) (out string) {
|
||||
}
|
||||
|
||||
func (n *Node) trim(item string) string {
|
||||
return strings.TrimSuffix(item, n.sep)
|
||||
return strings.ToLower(strings.TrimSuffix(item, n.sep))
|
||||
}
|
||||
|
||||
func (n *Node) Add(item string) {
|
||||
@@ -63,13 +64,18 @@ func (n *node) add(secs []string) {
|
||||
case 0:
|
||||
case 1:
|
||||
sec := secs[length-1]
|
||||
if idx := n.index(sec); idx >= 0 {
|
||||
if n.subNodes[idx] != nil {
|
||||
n.subNodes[idx].add([]string{""})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch sec {
|
||||
case "", "*", "**":
|
||||
n.secs = append([]string{sec}, n.secs...)
|
||||
n.subNodes = append([]*node{nil}, n.subNodes...)
|
||||
n.prepend(sec, nil)
|
||||
default:
|
||||
n.secs = append(n.secs, sec)
|
||||
n.subNodes = append(n.subNodes, nil)
|
||||
n.append(sec, nil)
|
||||
}
|
||||
default:
|
||||
sec := secs[length-1]
|
||||
@@ -81,13 +87,9 @@ func (n *node) add(secs []string) {
|
||||
if idx == -1 {
|
||||
switch sec {
|
||||
case "", "*", "**":
|
||||
idx = 0
|
||||
n.secs = append([]string{sec}, n.secs...)
|
||||
n.subNodes = append([]*node{{}}, n.subNodes...)
|
||||
idx = n.prepend(sec, &node{})
|
||||
default:
|
||||
idx = len(n.secs)
|
||||
n.secs = append(n.secs, sec)
|
||||
n.subNodes = append(n.subNodes, &node{})
|
||||
idx = n.append(sec, &node{})
|
||||
}
|
||||
|
||||
} else if n.subNodes[idx] == nil {
|
||||
@@ -104,10 +106,10 @@ func (n *Node) Match(item string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
return n.matchSecs(strings.Split(n.trim(item), n.sep), false)
|
||||
return n.matchSecs(strings.Split(n.trim(item), n.sep))
|
||||
}
|
||||
|
||||
func (n *node) matchSecs(secs []string, fuzzNode bool) bool {
|
||||
func (n *node) matchSecs(secs []string) bool {
|
||||
length := len(secs)
|
||||
if length == 0 {
|
||||
if n == nil {
|
||||
@@ -117,12 +119,12 @@ func (n *node) matchSecs(secs []string, fuzzNode bool) bool {
|
||||
}
|
||||
|
||||
if idx := n.index(secs[length-1]); idx >= 0 {
|
||||
if n.subNodes[idx].matchSecs(secs[:length-1], false) {
|
||||
if n.subNodes[idx].matchSecs(secs[:length-1]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if idx := n.index("*"); idx >= 0 {
|
||||
if n.subNodes[idx].matchSecs(secs[:length-1], true) {
|
||||
if n.subNodes[idx].matchSecs(secs[:length-1]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -134,10 +136,49 @@ func (n *node) index(sec string) int {
|
||||
if n == nil {
|
||||
return -1
|
||||
}
|
||||
for s := range n.secs {
|
||||
if n.secs[s] == sec {
|
||||
return s
|
||||
|
||||
if n.indexMap != nil {
|
||||
if idx, ok := n.indexMap[sec]; ok {
|
||||
return idx
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
for i := range n.secs {
|
||||
if n.secs[i] == sec {
|
||||
n.ensureIndexMap()
|
||||
n.indexMap[sec] = i
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func (n *node) append(sec string, child *node) int {
|
||||
n.secs = append(n.secs, sec)
|
||||
n.subNodes = append(n.subNodes, child)
|
||||
n.ensureIndexMap()
|
||||
idx := len(n.secs) - 1
|
||||
n.indexMap[sec] = idx
|
||||
return idx
|
||||
}
|
||||
|
||||
func (n *node) prepend(sec string, child *node) int {
|
||||
n.secs = append([]string{sec}, n.secs...)
|
||||
n.subNodes = append([]*node{child}, n.subNodes...)
|
||||
n.indexMap = nil
|
||||
n.ensureIndexMap()
|
||||
return 0
|
||||
}
|
||||
|
||||
func (n *node) ensureIndexMap() {
|
||||
if n.indexMap != nil {
|
||||
return
|
||||
}
|
||||
|
||||
n.indexMap = make(map[string]int, len(n.secs))
|
||||
for i := range n.secs {
|
||||
n.indexMap[n.secs[i]] = i
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package suffixtree_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/wweir/sower/pkg/suffixtree"
|
||||
"github.com/sower-proxy/sower/pkg/suffixtree"
|
||||
)
|
||||
|
||||
func TestNode_Match(t *testing.T) {
|
||||
@@ -74,6 +74,42 @@ func TestNode_Match(t *testing.T) {
|
||||
[]test{
|
||||
{"github.com", true},
|
||||
},
|
||||
}, {
|
||||
"normalize domain",
|
||||
suffixtree.NewNodeFromRules("GitHub.Com", "*.Example.Com", "**.Foo.Com"),
|
||||
[]test{
|
||||
{"github.com", true},
|
||||
{"GitHub.com", true},
|
||||
{"github.com.", true},
|
||||
{"a.example.com", true},
|
||||
{"A.Example.Com", true},
|
||||
{"foo.com", true},
|
||||
{"a.b.foo.com", true},
|
||||
},
|
||||
}, {
|
||||
"parent after child",
|
||||
suffixtree.NewNodeFromRules("a.wweir.cc", "wweir.cc"),
|
||||
[]test{
|
||||
{"wweir.cc", true},
|
||||
{"a.wweir.cc", true},
|
||||
{"b.wweir.cc", false},
|
||||
},
|
||||
}, {
|
||||
"wildcard after child",
|
||||
suffixtree.NewNodeFromRules("a.wweir.cc", "*.wweir.cc"),
|
||||
[]test{
|
||||
{"a.wweir.cc", true},
|
||||
{"b.wweir.cc", true},
|
||||
{"a.b.wweir.cc", false},
|
||||
},
|
||||
}, {
|
||||
"deep wildcard after child",
|
||||
suffixtree.NewNodeFromRules("a.b.foo.com", "**.foo.com"),
|
||||
[]test{
|
||||
{"foo.com", true},
|
||||
{"a.b.foo.com", true},
|
||||
{"z.foo.com", true},
|
||||
},
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package upstreamtls
|
||||
|
||||
import (
|
||||
cryptotls "crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
utls "github.com/refraction-networking/utls"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
ServerName string
|
||||
ClientHello string
|
||||
InsecureSkipVerify bool
|
||||
}
|
||||
|
||||
func ValidateClientHello(value string) error {
|
||||
_, err := clientHelloID(value)
|
||||
return err
|
||||
}
|
||||
|
||||
func Dial(dialer *net.Dialer, network, addr string, options Options) (net.Conn, error) {
|
||||
if options.ClientHello == "" {
|
||||
return cryptotls.DialWithDialer(dialer, network, addr, &cryptotls.Config{
|
||||
ServerName: options.ServerName,
|
||||
InsecureSkipVerify: options.InsecureSkipVerify,
|
||||
})
|
||||
}
|
||||
|
||||
helloID, err := clientHelloID(options.ClientHello)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawConn, err := dialer.Dial(network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if timeout := dialer.Timeout; timeout > 0 {
|
||||
if err := rawConn.SetDeadline(time.Now().Add(timeout)); err != nil {
|
||||
rawConn.Close()
|
||||
return nil, fmt.Errorf("set tls deadline: %w", err)
|
||||
}
|
||||
defer rawConn.SetDeadline(time.Time{})
|
||||
}
|
||||
|
||||
serverName := options.ServerName
|
||||
if serverName == "" {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
rawConn.Close()
|
||||
return nil, fmt.Errorf("split tls addr %q: %w", addr, err)
|
||||
}
|
||||
serverName = host
|
||||
}
|
||||
|
||||
conn := utls.UClient(rawConn, &utls.Config{
|
||||
ServerName: serverName,
|
||||
InsecureSkipVerify: options.InsecureSkipVerify,
|
||||
}, helloID)
|
||||
if err := conn.Handshake(); err != nil {
|
||||
rawConn.Close()
|
||||
return nil, fmt.Errorf("utls handshake: %w", err)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func clientHelloID(value string) (utls.ClientHelloID, error) {
|
||||
switch normalizeClientHello(value) {
|
||||
case "chrome":
|
||||
return utls.HelloChrome_Auto, nil
|
||||
case "firefox":
|
||||
return utls.HelloFirefox_Auto, nil
|
||||
case "ios":
|
||||
return utls.HelloIOS_Auto, nil
|
||||
case "android":
|
||||
return utls.HelloAndroid_11_OkHttp, nil
|
||||
case "edge":
|
||||
return utls.HelloEdge_Auto, nil
|
||||
case "safari":
|
||||
return utls.HelloSafari_Auto, nil
|
||||
case "360":
|
||||
return utls.Hello360_Auto, nil
|
||||
case "qq":
|
||||
return utls.HelloQQ_Auto, nil
|
||||
case "randomized":
|
||||
return utls.HelloRandomized, nil
|
||||
case "randomizedalpn":
|
||||
return utls.HelloRandomizedALPN, nil
|
||||
case "randomizednoalpn":
|
||||
return utls.HelloRandomizedNoALPN, nil
|
||||
case "golang":
|
||||
return utls.HelloGolang, nil
|
||||
default:
|
||||
return utls.ClientHelloID{}, fmt.Errorf("unsupported tls client hello %q", value)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeClientHello(value string) string {
|
||||
value = strings.TrimSpace(strings.ToLower(value))
|
||||
value = strings.ReplaceAll(value, "-", "")
|
||||
value = strings.ReplaceAll(value, "_", "")
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package upstreamtls
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestValidateClientHelloAcceptsAliases(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, value := range []string{"chrome", "firefox", "randomized_alpn", "randomized-no-alpn", "golang"} {
|
||||
if err := ValidateClientHello(value); err != nil {
|
||||
t.Fatalf("validate %q: %v", value, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateClientHelloRejectsUnknownValue(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if err := ValidateClientHello("unknown"); err == nil {
|
||||
t.Fatal("expected validation error")
|
||||
}
|
||||
}
|
||||
+3
-10
@@ -2,10 +2,9 @@ package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/sower-proxy/deferlog/log"
|
||||
)
|
||||
|
||||
func (r *Router) localSite(domain string) bool {
|
||||
@@ -17,10 +16,7 @@ func (r *Router) localSite(domain string) bool {
|
||||
|
||||
ips, err := net.DefaultResolver.LookupIP(ctx, "ip", domain)
|
||||
if err != nil || len(ips) == 0 {
|
||||
log.Warn().Err(err).
|
||||
Str("domain", domain).
|
||||
Int("ips", len(ips)).
|
||||
Msg("resolve domain")
|
||||
slog.Warn("resolve domain", "error", err, "domain", domain, "ips", len(ips))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -38,10 +34,7 @@ func (r *Router) localSite(domain string) bool {
|
||||
if r.country.Reader != nil {
|
||||
city, err := r.country.City(ip)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).
|
||||
Str("domain", domain).
|
||||
IPAddr("ip", ip).
|
||||
Msg("mmdb search")
|
||||
slog.Warn("mmdb search", "error", err, "domain", domain, "ip", ip)
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
+376
-56
@@ -1,37 +1,51 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sower-proxy/deferlog/log"
|
||||
"github.com/sower-proxy/mem"
|
||||
"github.com/wweir/sower/pkg/dhcp"
|
||||
)
|
||||
|
||||
var errNoLocalIPForQuestionType = errors.New("no local IP for question type")
|
||||
|
||||
func (r *Router) ServeDNS(w dns.ResponseWriter, req *dns.Msg) {
|
||||
// https://stackoverflow.com/questions/4082081/requesting-a-and-aaaa-records-in-single-dns-query/4083071#4083071
|
||||
if len(req.Question) == 0 {
|
||||
if len(req.Question) != 1 {
|
||||
_ = w.WriteMsg(r.dnsFail(req, dns.RcodeFormatError))
|
||||
return
|
||||
}
|
||||
|
||||
domain := req.Question[0].Name
|
||||
qtype := req.Question[0].Qtype
|
||||
// 1. rule_based( block > direct > proxy )
|
||||
switch {
|
||||
case r.BlockRule.Match(domain):
|
||||
if r.BlockRule.Match(domain) {
|
||||
_ = w.WriteMsg(r.dnsFail(req, dns.RcodeNameError))
|
||||
return
|
||||
|
||||
case r.DirectRule.Match(domain):
|
||||
|
||||
case r.ProxyRule.Match(domain):
|
||||
_ = w.WriteMsg(r.dnsProxyA(domain, r.dns.serveIP, req))
|
||||
return
|
||||
}
|
||||
|
||||
// 2. direct with cache, do not fallback to proxy to avoid side-effect
|
||||
if !r.DirectRule.Match(domain) && r.ProxyRule.Match(domain) {
|
||||
switch {
|
||||
case isAddressQuestion(qtype):
|
||||
resp, err := r.dnsProxyReply(domain, w.LocalAddr(), req)
|
||||
if err != nil {
|
||||
slog.Warn("proxy dns", "error", err, "domain", domain)
|
||||
_ = w.WriteMsg(r.dnsFail(req, dns.RcodeServerFailure))
|
||||
return
|
||||
}
|
||||
_ = w.WriteMsg(resp)
|
||||
return
|
||||
case isProxySensitiveQuestion(qtype):
|
||||
_ = w.WriteMsg(r.dnsNoData(req))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 2. direct query, do not fallback to proxy to avoid side-effect
|
||||
resp, err := r.Exchange(req)
|
||||
if err != nil {
|
||||
_ = w.WriteMsg(r.dnsFail(req, dns.RcodeServerFailure))
|
||||
@@ -42,67 +56,373 @@ func (r *Router) ServeDNS(w dns.ResponseWriter, req *dns.Msg) {
|
||||
|
||||
func (r *Router) dnsFail(req *dns.Msg, rcode int) *dns.Msg {
|
||||
m := new(dns.Msg)
|
||||
m.SetRcode(req, dns.RcodeServerFailure)
|
||||
m.SetRcode(req, rcode)
|
||||
return m
|
||||
}
|
||||
|
||||
func (r *Router) dnsProxyA(domain string, localIP net.IP, req *dns.Msg) *dns.Msg {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(req)
|
||||
func (r *Router) dnsNoData(req *dns.Msg) *dns.Msg {
|
||||
return dnsReply(req)
|
||||
}
|
||||
|
||||
if localIP.To4() != nil {
|
||||
m.Answer = []dns.RR{&dns.A{
|
||||
func (r *Router) dnsProxyReply(domain string, localAddr net.Addr, req *dns.Msg) (*dns.Msg, error) {
|
||||
qtype := req.Question[0].Qtype
|
||||
reply := dnsReply(req)
|
||||
|
||||
localIP, err := r.proxyReplyIP(localAddr, qtype)
|
||||
if err != nil {
|
||||
if errors.Is(err, errNoLocalIPForQuestionType) {
|
||||
return reply, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
record, err := proxyReplyRecord(domain, qtype, localIP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reply.Answer = []dns.RR{record}
|
||||
return reply, nil
|
||||
}
|
||||
|
||||
func (r *Router) proxyReplyIP(localAddr net.Addr, qtype uint16) (net.IP, error) {
|
||||
host, _, err := net.SplitHostPort(localAddr.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("split local dns address %q: %w", localAddr.String(), err)
|
||||
}
|
||||
|
||||
ip := net.ParseIP(strings.Trim(host, "[]"))
|
||||
if ip != nil && !ip.IsUnspecified() && matchesQuestionTypeIP(ip, qtype) {
|
||||
return ip, nil
|
||||
}
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("parse local dns IP %q", host)
|
||||
}
|
||||
if !isAddressQuestion(qtype) {
|
||||
return nil, fmt.Errorf("unsupported question type %d", qtype)
|
||||
}
|
||||
|
||||
for _, serveIP := range r.dns.serveIPs {
|
||||
if !serveIP.IsUnspecified() && matchesQuestionTypeIP(serveIP, qtype) {
|
||||
return serveIP, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w %d", errNoLocalIPForQuestionType, qtype)
|
||||
}
|
||||
|
||||
func isAddressQuestion(qtype uint16) bool {
|
||||
return qtype == dns.TypeA || qtype == dns.TypeAAAA
|
||||
}
|
||||
|
||||
func isProxySensitiveQuestion(qtype uint16) bool {
|
||||
return qtype == dns.TypeHTTPS || qtype == dns.TypeSVCB
|
||||
}
|
||||
|
||||
func dnsReply(req *dns.Msg) *dns.Msg {
|
||||
reply := new(dns.Msg)
|
||||
reply.SetReply(req)
|
||||
return reply
|
||||
}
|
||||
|
||||
func proxyReplyRecord(domain string, qtype uint16, localIP net.IP) (dns.RR, error) {
|
||||
switch qtype {
|
||||
case dns.TypeA:
|
||||
return &dns.A{
|
||||
Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 20},
|
||||
A: localIP,
|
||||
}}
|
||||
|
||||
} else {
|
||||
m.Answer = []dns.RR{&dns.AAAA{
|
||||
}, nil
|
||||
case dns.TypeAAAA:
|
||||
return &dns.AAAA{
|
||||
Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 20},
|
||||
AAAA: localIP,
|
||||
}}
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported question type %d", qtype)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
var upstreamAddrs []string
|
||||
var upstreamIndex = -1
|
||||
var dhcpCache = mem.NewRotateCache(10*time.Minute, func(int) ([]string, error) {
|
||||
return dhcp.GetDNSServer()
|
||||
})
|
||||
var queryCount = 0
|
||||
func matchesQuestionTypeIP(ip net.IP, qtype uint16) bool {
|
||||
switch qtype {
|
||||
case dns.TypeA:
|
||||
return ip.To4() != nil
|
||||
case dns.TypeAAAA:
|
||||
return ip.To4() == nil
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
dnsTimeout = 2 * time.Second
|
||||
dnsUDPSize = 1232
|
||||
dnsRetryInterval = 30 * time.Second
|
||||
dnsRefreshTTL = 5 * time.Minute
|
||||
)
|
||||
|
||||
func (r *Router) Exchange(req *dns.Msg) (_ *dns.Msg, err error) {
|
||||
queryCount++
|
||||
if upstreamIndex < 0 {
|
||||
dnsIPs, err := dhcpCache.Get(0)
|
||||
log.Err(err).
|
||||
Int("queryCount", queryCount).
|
||||
Strs("dns", dnsIPs).
|
||||
Msg("get dns server")
|
||||
|
||||
addrs := make([]string, 0, len(dnsIPs)+1)
|
||||
for _, ip := range dnsIPs {
|
||||
if ip != string(r.dns.serveIP) {
|
||||
addrs = append(addrs, net.JoinHostPort(ip, "53"))
|
||||
}
|
||||
}
|
||||
if len(addrs) == 0 {
|
||||
addrs = append(addrs, net.JoinHostPort(r.dns.fallbackDNS, "53"))
|
||||
}
|
||||
|
||||
queryCount = 0
|
||||
upstreamAddrs = addrs
|
||||
upstreamIndex = len(addrs) - 1
|
||||
addrs, index, shouldProbe, err := r.currentUpstreamState(time.Now())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := dns.Exchange(req, upstreamAddrs[upstreamIndex])
|
||||
if shouldProbe {
|
||||
resp, probeErr := r.exchangeWithRetry(req, addrs[0])
|
||||
if probeErr == nil {
|
||||
r.promoteUpstream()
|
||||
return resp, nil
|
||||
}
|
||||
r.scheduleRetry(time.Now())
|
||||
}
|
||||
|
||||
resp, err := r.exchangeWithRetry(req, addrs[index])
|
||||
if err != nil {
|
||||
upstreamIndex--
|
||||
if upstreamIndex >= 0 {
|
||||
resp, err = dns.Exchange(req, upstreamAddrs[upstreamIndex])
|
||||
nextIndex, switched := r.degradeUpstream(index)
|
||||
if switched {
|
||||
slog.Info("use upstream dns", "ip", addrs[nextIndex])
|
||||
resp, err = r.exchangeWithRetry(req, addrs[nextIndex])
|
||||
}
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (r *Router) currentUpstreamState(now time.Time) ([]string, int, bool, error) {
|
||||
for {
|
||||
addrs, index, shouldProbe, needRefresh, err := r.prepareUpstreamState(now)
|
||||
if !needRefresh || err != nil {
|
||||
return addrs, index, shouldProbe, err
|
||||
}
|
||||
|
||||
refreshedAddrs, refreshErr := r.buildUpstreamAddrs()
|
||||
r.finishUpstreamRefresh(now, refreshedAddrs, refreshErr)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) degradeUpstream(index int) (int, bool) {
|
||||
r.dns.Lock()
|
||||
defer r.dns.Unlock()
|
||||
|
||||
if len(r.dns.upstreamAddrs) == 0 || r.dns.upstreamIndex != index || index >= len(r.dns.upstreamAddrs)-1 {
|
||||
return r.dns.upstreamIndex, false
|
||||
}
|
||||
|
||||
r.dns.upstreamIndex = index + 1
|
||||
r.dns.retryAt = time.Now().Add(dnsRetryInterval)
|
||||
r.dns.probeInFlight = false
|
||||
return r.dns.upstreamIndex, true
|
||||
}
|
||||
|
||||
func (r *Router) promoteUpstream() {
|
||||
r.dns.Lock()
|
||||
defer r.dns.Unlock()
|
||||
|
||||
r.dns.upstreamIndex = 0
|
||||
r.dns.retryAt = time.Time{}
|
||||
r.dns.probeInFlight = false
|
||||
}
|
||||
|
||||
func (r *Router) scheduleRetry(now time.Time) {
|
||||
r.dns.Lock()
|
||||
defer r.dns.Unlock()
|
||||
|
||||
if r.dns.upstreamIndex > 0 {
|
||||
r.dns.retryAt = now.Add(dnsRetryInterval)
|
||||
r.dns.probeInFlight = false
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) prepareUpstreamState(now time.Time) ([]string, int, bool, bool, error) {
|
||||
r.dns.Lock()
|
||||
defer r.dns.Unlock()
|
||||
|
||||
needRefresh := false
|
||||
switch {
|
||||
case len(r.dns.upstreamAddrs) == 0:
|
||||
if r.dns.refreshInFlight {
|
||||
if fallbackAddrs, ok := r.fallbackOnlyUpstreamsLocked(); ok {
|
||||
return fallbackAddrs, 0, false, false, nil
|
||||
}
|
||||
return nil, 0, false, false, r.upstreamUnavailableErrLocked()
|
||||
}
|
||||
if !r.dns.refreshAt.IsZero() && now.Before(r.dns.refreshAt) {
|
||||
if fallbackAddrs, ok := r.fallbackOnlyUpstreamsLocked(); ok {
|
||||
return fallbackAddrs, 0, false, false, nil
|
||||
}
|
||||
return nil, 0, false, false, r.upstreamUnavailableErrLocked()
|
||||
}
|
||||
r.dns.refreshInFlight = true
|
||||
needRefresh = true
|
||||
case r.shouldRefreshUpstreams(now) && !r.dns.refreshInFlight:
|
||||
r.dns.refreshInFlight = true
|
||||
needRefresh = true
|
||||
}
|
||||
|
||||
shouldProbe := !needRefresh &&
|
||||
len(r.dns.upstreamAddrs) > 0 &&
|
||||
r.dns.upstreamIndex > 0 &&
|
||||
!r.dns.retryAt.IsZero() &&
|
||||
!now.Before(r.dns.retryAt) &&
|
||||
!r.dns.probeInFlight
|
||||
if shouldProbe {
|
||||
r.dns.probeInFlight = true
|
||||
r.dns.retryAt = now.Add(dnsRetryInterval)
|
||||
}
|
||||
|
||||
addrs := append([]string(nil), r.dns.upstreamAddrs...)
|
||||
return addrs, r.dns.upstreamIndex, shouldProbe, needRefresh, nil
|
||||
}
|
||||
|
||||
func (r *Router) buildUpstreamAddrs() ([]string, error) {
|
||||
dnsIPs := []string{r.dns.upstreamDNS}
|
||||
if r.dns.upstreamDNS == "" {
|
||||
var err error
|
||||
dnsIPs, err = r.dns.getDNSServer()
|
||||
if err != nil {
|
||||
slog.Error("get dns server", "error", err, "dns", dnsIPs)
|
||||
}
|
||||
}
|
||||
|
||||
addrs := make([]string, 0, len(dnsIPs)+1)
|
||||
seen := make(map[string]struct{}, len(dnsIPs)+1)
|
||||
appendAddr := func(ip string) {
|
||||
if ip == "" || r.isServeIP(ip) {
|
||||
return
|
||||
}
|
||||
addr := net.JoinHostPort(ip, "53")
|
||||
if _, ok := seen[addr]; ok {
|
||||
return
|
||||
}
|
||||
seen[addr] = struct{}{}
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
|
||||
for _, ip := range dnsIPs {
|
||||
appendAddr(ip)
|
||||
}
|
||||
appendAddr(r.dns.fallbackDNS)
|
||||
if len(addrs) == 0 {
|
||||
return nil, fmt.Errorf("no available upstream dns")
|
||||
}
|
||||
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
func (r *Router) shouldRefreshUpstreams(now time.Time) bool {
|
||||
return r.dns.upstreamDNS == "" && !r.dns.refreshAt.IsZero() && !now.Before(r.dns.refreshAt)
|
||||
}
|
||||
|
||||
func (r *Router) scheduleRefreshLocked(now time.Time) {
|
||||
if r.dns.upstreamDNS == "" {
|
||||
r.dns.refreshAt = now.Add(dnsRefreshTTL)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) applyUpstreamAddrsLocked(addrs []string, now time.Time) {
|
||||
currentAddr := ""
|
||||
retryAt := r.dns.retryAt
|
||||
if len(r.dns.upstreamAddrs) > 0 && r.dns.upstreamIndex >= 0 && r.dns.upstreamIndex < len(r.dns.upstreamAddrs) {
|
||||
currentAddr = r.dns.upstreamAddrs[r.dns.upstreamIndex]
|
||||
}
|
||||
|
||||
r.dns.upstreamAddrs = addrs
|
||||
r.dns.upstreamIndex = 0
|
||||
if currentAddr != "" {
|
||||
for i, addr := range addrs {
|
||||
if addr == currentAddr {
|
||||
r.dns.upstreamIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if r.dns.upstreamIndex == 0 {
|
||||
r.dns.retryAt = time.Time{}
|
||||
} else {
|
||||
r.dns.retryAt = retryAt
|
||||
}
|
||||
r.dns.lastRefreshErr = nil
|
||||
r.dns.refreshInFlight = false
|
||||
r.dns.probeInFlight = false
|
||||
r.scheduleRefreshLocked(now)
|
||||
slog.Info("use upstream dns", "ips", addrs)
|
||||
}
|
||||
|
||||
func (r *Router) finishUpstreamRefresh(now time.Time, addrs []string, err error) {
|
||||
r.dns.Lock()
|
||||
defer r.dns.Unlock()
|
||||
|
||||
if err != nil {
|
||||
r.dns.refreshInFlight = false
|
||||
r.dns.lastRefreshErr = err
|
||||
r.scheduleRefreshLocked(now)
|
||||
if len(r.dns.upstreamAddrs) > 0 {
|
||||
slog.Warn("refresh upstream dns", "error", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
r.applyUpstreamAddrsLocked(addrs, now)
|
||||
}
|
||||
|
||||
func (r *Router) upstreamUnavailableErrLocked() error {
|
||||
if r.dns.lastRefreshErr != nil {
|
||||
return r.dns.lastRefreshErr
|
||||
}
|
||||
return fmt.Errorf("upstream dns unavailable")
|
||||
}
|
||||
|
||||
func (r *Router) fallbackOnlyUpstreamsLocked() ([]string, bool) {
|
||||
if r.dns.fallbackDNS == "" || r.isServeIP(r.dns.fallbackDNS) {
|
||||
return nil, false
|
||||
}
|
||||
return []string{net.JoinHostPort(r.dns.fallbackDNS, "53")}, true
|
||||
}
|
||||
|
||||
func (r *Router) exchangeWithRetry(req *dns.Msg, addr string) (*dns.Msg, error) {
|
||||
resp, err := r.exchangeUDP(req, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !resp.Truncated {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
return r.exchangeTCP(req, addr)
|
||||
}
|
||||
|
||||
func (r *Router) exchangeUDP(req *dns.Msg, addr string) (*dns.Msg, error) {
|
||||
msg := req.Copy()
|
||||
if msg.IsEdns0() == nil {
|
||||
msg.SetEdns0(dnsUDPSize, false)
|
||||
}
|
||||
|
||||
client := dns.Client{
|
||||
Net: "udp",
|
||||
Timeout: dnsTimeout,
|
||||
UDPSize: dnsUDPSize,
|
||||
}
|
||||
resp, _, err := client.Exchange(msg, addr)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (r *Router) exchangeTCP(req *dns.Msg, addr string) (*dns.Msg, error) {
|
||||
client := dns.Client{
|
||||
Net: "tcp",
|
||||
Timeout: dnsTimeout,
|
||||
}
|
||||
resp, _, err := client.Exchange(req.Copy(), addr)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (r *Router) isServeIP(raw string) bool {
|
||||
ip := net.ParseIP(strings.Trim(raw, "[]"))
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, serveIP := range r.dns.serveIPs {
|
||||
if serveIP.Equal(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
+28
-21
@@ -1,11 +1,11 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sower-proxy/deferlog/log"
|
||||
"github.com/sower-proxy/mem"
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ func (r *Router) isAccess(domain string, port uint16) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
ok, _ := accessCache.Get(domain)
|
||||
ok, _ := accessCache.Get(accessCacheKey(domain, port))
|
||||
return ok
|
||||
}
|
||||
|
||||
@@ -28,24 +28,31 @@ var pingClient = http.Client{
|
||||
}
|
||||
|
||||
func httpPing(key string) (bool, error) {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
var err80, err443 error
|
||||
go func() {
|
||||
_, err80 = pingClient.Head("http://" + key)
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
_, err443 = pingClient.Head("https://" + key)
|
||||
wg.Done()
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
if err80 != nil && err443 != nil {
|
||||
log.Warn().
|
||||
Errs("errs", []error{err80, err443}).
|
||||
Msg("Failed to ping")
|
||||
scheme, host, err := accessProbeTarget(key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return err80 == nil && err443 == nil, nil
|
||||
resp, err := pingClient.Head(scheme + "://" + host)
|
||||
if err != nil {
|
||||
slog.Warn("failed to ping", "error", err, "scheme", scheme, "host", host)
|
||||
return false, nil
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func accessCacheKey(domain string, port uint16) string {
|
||||
return fmt.Sprintf("%d:%s", port, domain)
|
||||
}
|
||||
|
||||
func accessProbeTarget(key string) (string, string, error) {
|
||||
switch {
|
||||
case len(key) > 3 && key[:3] == "80:":
|
||||
return "http", key[3:], nil
|
||||
case len(key) > 4 && key[:4] == "443:":
|
||||
return "https", key[4:], nil
|
||||
default:
|
||||
return "", "", fmt.Errorf("invalid access probe key %q", key)
|
||||
}
|
||||
}
|
||||
|
||||
+79
-47
@@ -1,49 +1,73 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
geoip2 "github.com/oschwald/geoip2-golang"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sower-proxy/conns/relay"
|
||||
"github.com/sower-proxy/deferlog"
|
||||
"github.com/sower-proxy/deferlog/log"
|
||||
"github.com/wweir/sower/pkg/suffixtree"
|
||||
"github.com/sower-proxy/deferlog/v2"
|
||||
"github.com/sower-proxy/sower/pkg/dhcp"
|
||||
"github.com/sower-proxy/sower/pkg/suffixtree"
|
||||
)
|
||||
|
||||
type ProxyDialFn func(network, host string, port uint16) (net.Conn, error)
|
||||
type Router struct {
|
||||
BlockRule *suffixtree.Node
|
||||
DirectRule *suffixtree.Node
|
||||
ProxyRule *suffixtree.Node
|
||||
ProxyDial ProxyDialFn
|
||||
type (
|
||||
ProxyDialFn func(network, host string, port uint16) (net.Conn, error)
|
||||
Router struct {
|
||||
BlockRule *suffixtree.Node
|
||||
DirectRule *suffixtree.Node
|
||||
ProxyRule *suffixtree.Node
|
||||
ProxyDial ProxyDialFn
|
||||
|
||||
dns struct {
|
||||
dns.Client
|
||||
fallbackDNS string
|
||||
serveIP net.IP
|
||||
dns struct {
|
||||
upstreamDNS string
|
||||
fallbackDNS string
|
||||
serveIPs []net.IP
|
||||
getDNSServer func() ([]string, error)
|
||||
|
||||
sync.Mutex
|
||||
upstreamAddrs []string
|
||||
upstreamIndex int
|
||||
refreshAt time.Time
|
||||
refreshInFlight bool
|
||||
lastRefreshErr error
|
||||
retryAt time.Time
|
||||
probeInFlight bool
|
||||
}
|
||||
|
||||
country struct {
|
||||
*geoip2.Reader
|
||||
cidrs []*net.IPNet
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
country struct {
|
||||
*geoip2.Reader
|
||||
cidrs []*net.IPNet
|
||||
}
|
||||
}
|
||||
var ErrBlocked = errors.New("route blocked")
|
||||
|
||||
func NewRouter(serveIP, fallbackDNS, mmdbFile string, proxyDial ProxyDialFn) *Router {
|
||||
func NewRouter(serveIPs []string, upstreamDNS, fallbackDNS, mmdbFile string, proxyDial ProxyDialFn) *Router {
|
||||
r := Router{
|
||||
ProxyDial: proxyDial,
|
||||
}
|
||||
|
||||
r.dns.serveIP = net.ParseIP(serveIP)
|
||||
r.dns.upstreamDNS = upstreamDNS
|
||||
r.dns.fallbackDNS = fallbackDNS
|
||||
r.dns.getDNSServer = dhcp.GetDNSServer
|
||||
for _, serveIP := range serveIPs {
|
||||
if ip := net.ParseIP(serveIP); ip != nil {
|
||||
r.dns.serveIPs = append(r.dns.serveIPs, ip)
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
r.country.Reader, err = geoip2.Open(mmdbFile)
|
||||
log.InfoWarn(err).Str("file", mmdbFile).Msg("open geoip2 db")
|
||||
if err != nil {
|
||||
slog.Warn("open geoip2 db", "error", err, "file", mmdbFile)
|
||||
}
|
||||
|
||||
return &r
|
||||
}
|
||||
@@ -52,7 +76,8 @@ func (r *Router) AddCountryCIDRs(cidrs ...string) {
|
||||
for _, cidr := range cidrs {
|
||||
_, ipnet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to parse CIDR")
|
||||
slog.Error("failed to parse CIDR", "error", err, "cidr", cidr)
|
||||
continue
|
||||
}
|
||||
r.country.cidrs = append(r.country.cidrs, ipnet)
|
||||
}
|
||||
@@ -62,13 +87,22 @@ func (r *Router) AddCountryCIDRs(cidrs ...string) {
|
||||
func (r *Router) RouteHandle(conn net.Conn, domain string, port uint16) (err error) {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
deferlog.DebugWarn(err).
|
||||
Str("domain", domain).
|
||||
Uint16("port", port).
|
||||
Dur("spend", time.Since(start)).
|
||||
Msg("serve socks5")
|
||||
deferlog.DebugWarn(err, "route handle", "domain", domain, "port", port, "took", time.Since(start))
|
||||
}()
|
||||
|
||||
rc, err := r.Dial("tcp", domain, port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
if err := relay.Relay(conn, rc); err != nil {
|
||||
return fmt.Errorf("relay %s:%d: %w", domain, port, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Router) Dial(network, domain string, port uint16) (net.Conn, error) {
|
||||
addr := net.JoinHostPort(domain, strconv.FormatUint(uint64(port), 10))
|
||||
|
||||
// 1. rule_based( block > direct > proxy )
|
||||
@@ -76,34 +110,32 @@ func (r *Router) RouteHandle(conn net.Conn, domain string, port uint16) (err err
|
||||
// 3. fallback( proxy )
|
||||
switch {
|
||||
case r.BlockRule.Match(domain):
|
||||
return nil
|
||||
|
||||
return nil, ErrBlocked
|
||||
case r.DirectRule.Match(domain):
|
||||
return r.DirectHandle(conn, addr)
|
||||
|
||||
return r.directDial(network, addr)
|
||||
case r.ProxyRule.Match(domain):
|
||||
return r.ProxyHandle(conn, domain, port)
|
||||
|
||||
return r.proxyDial(network, domain, port)
|
||||
case r.localSite(domain), r.isAccess(domain, port):
|
||||
return r.DirectHandle(conn, addr)
|
||||
return r.directDial(network, addr)
|
||||
default:
|
||||
return r.ProxyHandle(conn, domain, port)
|
||||
return r.proxyDial(network, domain, port)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) ProxyHandle(conn net.Conn, domain string, port uint16) error {
|
||||
func (r *Router) proxyDial(network, domain string, port uint16) (net.Conn, error) {
|
||||
start := time.Now()
|
||||
rc, err := r.ProxyDial("tcp", domain, port)
|
||||
rc, err := r.ProxyDial(network, domain, port)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "proxy dial (%s:%d), spend (%s)", domain, port, time.Since(start))
|
||||
return nil, fmt.Errorf("proxy dial %s:%d, spend (%s): %w", domain, port, time.Since(start), err)
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
relay.Relay(conn, rc)
|
||||
return nil
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
func (r *Router) DirectHandle(conn net.Conn, addr string) error {
|
||||
dur, err := relay.RelayTo(conn, addr)
|
||||
return errors.Wrapf(err, "spend (%s)", dur)
|
||||
func (r *Router) directDial(network, addr string) (net.Conn, error) {
|
||||
start := time.Now()
|
||||
conn, err := net.DialTimeout(network, addr, 5*time.Second)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("direct dial %s, spend (%s): %w", addr, time.Since(start), err)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,783 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sower-proxy/mem"
|
||||
"github.com/sower-proxy/sower/pkg/suffixtree"
|
||||
)
|
||||
|
||||
type roundTripFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (fn roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return fn(req)
|
||||
}
|
||||
|
||||
func TestDNSProxyReplyMatchesQuestionType(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1", "::1"}, "", "223.5.5.5", "", nil)
|
||||
|
||||
reqA := new(dns.Msg)
|
||||
reqA.SetQuestion("example.com.", dns.TypeA)
|
||||
respA, err := r.dnsProxyReply("example.com.", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 53}, reqA)
|
||||
if err != nil {
|
||||
t.Fatalf("dnsProxyReply A failed: %v", err)
|
||||
}
|
||||
if len(respA.Answer) != 1 {
|
||||
t.Fatalf("unexpected A answer count: %d", len(respA.Answer))
|
||||
}
|
||||
if _, ok := respA.Answer[0].(*dns.A); !ok {
|
||||
t.Fatalf("expected A record, got %T", respA.Answer[0])
|
||||
}
|
||||
|
||||
reqAAAA := new(dns.Msg)
|
||||
reqAAAA.SetQuestion("example.com.", dns.TypeAAAA)
|
||||
respAAAA, err := r.dnsProxyReply("example.com.", &net.UDPAddr{IP: net.ParseIP("::1"), Port: 53}, reqAAAA)
|
||||
if err != nil {
|
||||
t.Fatalf("dnsProxyReply AAAA failed: %v", err)
|
||||
}
|
||||
if len(respAAAA.Answer) != 1 {
|
||||
t.Fatalf("unexpected AAAA answer count: %d", len(respAAAA.Answer))
|
||||
}
|
||||
if _, ok := respAAAA.Answer[0].(*dns.AAAA); !ok {
|
||||
t.Fatalf("expected AAAA record, got %T", respAAAA.Answer[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSProxyReplyPrefersRequestLocalAddr(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1", "127.0.0.2"}, "", "223.5.5.5", "", nil)
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("example.com.", dns.TypeA)
|
||||
|
||||
resp, err := r.dnsProxyReply("example.com.", &net.UDPAddr{IP: net.ParseIP("127.0.0.2"), Port: 53}, req)
|
||||
if err != nil {
|
||||
t.Fatalf("dnsProxyReply failed: %v", err)
|
||||
}
|
||||
|
||||
answer, ok := resp.Answer[0].(*dns.A)
|
||||
if !ok {
|
||||
t.Fatalf("expected A record, got %T", resp.Answer[0])
|
||||
}
|
||||
if !answer.A.Equal(net.ParseIP("127.0.0.2")) {
|
||||
t.Fatalf("expected request local IP 127.0.0.2, got %s", answer.A)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSProxyReplyIgnoresUnspecifiedLocalAddr(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter([]string{"127.0.0.2", "0.0.0.0"}, "", "223.5.5.5", "", nil)
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("example.com.", dns.TypeA)
|
||||
|
||||
resp, err := r.dnsProxyReply("example.com.", &net.UDPAddr{IP: net.ParseIP("0.0.0.0"), Port: 53}, req)
|
||||
if err != nil {
|
||||
t.Fatalf("dnsProxyReply failed: %v", err)
|
||||
}
|
||||
|
||||
answer, ok := resp.Answer[0].(*dns.A)
|
||||
if !ok {
|
||||
t.Fatalf("expected A record, got %T", resp.Answer[0])
|
||||
}
|
||||
if !answer.A.Equal(net.ParseIP("127.0.0.2")) {
|
||||
t.Fatalf("expected configured serve IP 127.0.0.2, got %s", answer.A)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExchangeSkipsServeIPInUpstreamList(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1"}, "127.0.0.1", "127.0.0.2", "", nil)
|
||||
addrs, err := r.buildUpstreamAddrs()
|
||||
if err != nil {
|
||||
t.Fatalf("buildUpstreamAddrs failed: %v", err)
|
||||
}
|
||||
|
||||
if len(addrs) != 1 {
|
||||
t.Fatalf("unexpected upstream list: %v", addrs)
|
||||
}
|
||||
if addrs[0] != "127.0.0.2:53" {
|
||||
t.Fatalf("expected fallback only, got %v", addrs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildUpstreamAddrsPrefersConfiguredDNSBeforeFallback(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1"}, "1.1.1.1", "9.9.9.9", "", nil)
|
||||
addrs, err := r.buildUpstreamAddrs()
|
||||
if err != nil {
|
||||
t.Fatalf("buildUpstreamAddrs failed: %v", err)
|
||||
}
|
||||
|
||||
want := []string{"1.1.1.1:53", "9.9.9.9:53"}
|
||||
if strings.Join(addrs, ",") != strings.Join(want, ",") {
|
||||
t.Fatalf("unexpected upstream order: got %v want %v", addrs, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddCountryCIDRsSkipsInvalidEntries(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter(nil, "", "223.5.5.5", "", nil)
|
||||
r.AddCountryCIDRs("invalid-cidr")
|
||||
|
||||
if len(r.country.cidrs) != 0 {
|
||||
t.Fatalf("expected invalid CIDR to be skipped, got %d entries", len(r.country.cidrs))
|
||||
}
|
||||
if r.localSite("127.0.0.1") {
|
||||
t.Fatal("unexpected localSite match without valid CIDRs or MMDB")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAccessChecksRequestedPortOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
origTransport := pingClient.Transport
|
||||
origCache := accessCache
|
||||
t.Cleanup(func() {
|
||||
pingClient.Transport = origTransport
|
||||
accessCache = origCache
|
||||
})
|
||||
|
||||
pingClient.Transport = roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
if req.URL.Scheme != "https" {
|
||||
return nil, errors.New("scheme unavailable")
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader("")),
|
||||
Header: make(http.Header),
|
||||
}, nil
|
||||
})
|
||||
accessCache = mem.NewRotateCache(time.Hour, httpPing)
|
||||
|
||||
r := NewRouter(nil, "", "223.5.5.5", "", nil)
|
||||
if !r.isAccess("example.com", 443) {
|
||||
t.Fatal("expected HTTPS probe to succeed")
|
||||
}
|
||||
if r.isAccess("example.com", 80) {
|
||||
t.Fatal("expected HTTP probe to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeDNSForProxyRuleForwardsNonAddressQuestion(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
udpConn, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen udp: %v", err)
|
||||
}
|
||||
defer udpConn.Close()
|
||||
|
||||
handler := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
resp := new(dns.Msg)
|
||||
resp.SetReply(req)
|
||||
resp.Answer = []dns.RR{&dns.TXT{
|
||||
Hdr: dns.RR_Header{Name: req.Question[0].Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 20},
|
||||
Txt: []string{"forwarded"},
|
||||
}}
|
||||
_ = w.WriteMsg(resp)
|
||||
})
|
||||
|
||||
server := &dns.Server{PacketConn: udpConn, Handler: handler}
|
||||
go func() { _ = server.ActivateAndServe() }()
|
||||
t.Cleanup(func() {
|
||||
_ = server.Shutdown()
|
||||
})
|
||||
|
||||
r := NewRouter([]string{"127.0.0.2"}, "", "223.5.5.5", "", nil)
|
||||
r.BlockRule = suffixtree.NewNodeFromRules()
|
||||
r.DirectRule = suffixtree.NewNodeFromRules()
|
||||
r.ProxyRule = suffixtree.NewNodeFromRules("example.com.")
|
||||
r.dns.upstreamAddrs = []string{udpConn.LocalAddr().String()}
|
||||
r.dns.upstreamIndex = 0
|
||||
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("example.com.", dns.TypeTXT)
|
||||
writer := &mockDNSWriter{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.2"), Port: 53}}
|
||||
|
||||
r.ServeDNS(writer, req)
|
||||
|
||||
if writer.msg == nil {
|
||||
t.Fatal("expected response message")
|
||||
}
|
||||
if writer.msg.Rcode != dns.RcodeSuccess {
|
||||
t.Fatalf("expected successful response, got rcode %d", writer.msg.Rcode)
|
||||
}
|
||||
if len(writer.msg.Answer) != 1 {
|
||||
t.Fatalf("expected forwarded TXT answer, got %d answers", len(writer.msg.Answer))
|
||||
}
|
||||
answer, ok := writer.msg.Answer[0].(*dns.TXT)
|
||||
if !ok {
|
||||
t.Fatalf("expected TXT record, got %T", writer.msg.Answer[0])
|
||||
}
|
||||
if len(answer.Txt) != 1 || answer.Txt[0] != "forwarded" {
|
||||
t.Fatalf("unexpected TXT payload: %v", answer.Txt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeDNSForProxyRuleSuppressesHTTPSQuestion(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
udpConn, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen udp: %v", err)
|
||||
}
|
||||
defer udpConn.Close()
|
||||
|
||||
var upstreamQueries atomic.Int32
|
||||
handler := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
upstreamQueries.Add(1)
|
||||
resp := new(dns.Msg)
|
||||
resp.SetReply(req)
|
||||
_ = w.WriteMsg(resp)
|
||||
})
|
||||
|
||||
server := &dns.Server{PacketConn: udpConn, Handler: handler}
|
||||
go func() { _ = server.ActivateAndServe() }()
|
||||
t.Cleanup(func() {
|
||||
_ = server.Shutdown()
|
||||
})
|
||||
|
||||
r := NewRouter([]string{"127.0.0.2"}, "", "223.5.5.5", "", nil)
|
||||
r.BlockRule = suffixtree.NewNodeFromRules()
|
||||
r.DirectRule = suffixtree.NewNodeFromRules()
|
||||
r.ProxyRule = suffixtree.NewNodeFromRules("example.com.")
|
||||
r.dns.upstreamAddrs = []string{udpConn.LocalAddr().String()}
|
||||
r.dns.upstreamIndex = 0
|
||||
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("example.com.", dns.TypeHTTPS)
|
||||
writer := &mockDNSWriter{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.2"), Port: 53}}
|
||||
|
||||
r.ServeDNS(writer, req)
|
||||
|
||||
if writer.msg == nil {
|
||||
t.Fatal("expected response message")
|
||||
}
|
||||
if writer.msg.Rcode != dns.RcodeSuccess {
|
||||
t.Fatalf("expected successful response, got rcode %d", writer.msg.Rcode)
|
||||
}
|
||||
if len(writer.msg.Answer) != 0 {
|
||||
t.Fatalf("expected NODATA response, got %d answers", len(writer.msg.Answer))
|
||||
}
|
||||
if upstreamQueries.Load() != 0 {
|
||||
t.Fatalf("expected HTTPS query to be suppressed locally, got %d upstream queries", upstreamQueries.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeDNSForProxyRuleForwardsSRVQuestion(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
udpConn, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen udp: %v", err)
|
||||
}
|
||||
defer udpConn.Close()
|
||||
|
||||
var upstreamQueries atomic.Int32
|
||||
handler := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
upstreamQueries.Add(1)
|
||||
resp := new(dns.Msg)
|
||||
resp.SetReply(req)
|
||||
resp.Answer = []dns.RR{&dns.SRV{
|
||||
Hdr: dns.RR_Header{Name: req.Question[0].Name, Rrtype: dns.TypeSRV, Class: dns.ClassINET, Ttl: 20},
|
||||
Priority: 10,
|
||||
Weight: 5,
|
||||
Port: 5222,
|
||||
Target: "srv.example.com.",
|
||||
}}
|
||||
_ = w.WriteMsg(resp)
|
||||
})
|
||||
|
||||
server := &dns.Server{PacketConn: udpConn, Handler: handler}
|
||||
go func() { _ = server.ActivateAndServe() }()
|
||||
t.Cleanup(func() {
|
||||
_ = server.Shutdown()
|
||||
})
|
||||
|
||||
r := NewRouter([]string{"127.0.0.2"}, "", "223.5.5.5", "", nil)
|
||||
r.BlockRule = suffixtree.NewNodeFromRules()
|
||||
r.DirectRule = suffixtree.NewNodeFromRules()
|
||||
r.ProxyRule = suffixtree.NewNodeFromRules("example.com.")
|
||||
r.dns.upstreamAddrs = []string{udpConn.LocalAddr().String()}
|
||||
r.dns.upstreamIndex = 0
|
||||
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("_xmpp-client._tcp.example.com.", dns.TypeSRV)
|
||||
writer := &mockDNSWriter{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.2"), Port: 53}}
|
||||
|
||||
r.ServeDNS(writer, req)
|
||||
|
||||
if writer.msg == nil {
|
||||
t.Fatal("expected response message")
|
||||
}
|
||||
if writer.msg.Rcode != dns.RcodeSuccess {
|
||||
t.Fatalf("expected successful response, got rcode %d", writer.msg.Rcode)
|
||||
}
|
||||
if len(writer.msg.Answer) != 1 {
|
||||
t.Fatalf("expected forwarded SRV answer, got %d answers", len(writer.msg.Answer))
|
||||
}
|
||||
answer, ok := writer.msg.Answer[0].(*dns.SRV)
|
||||
if !ok {
|
||||
t.Fatalf("expected SRV record, got %T", writer.msg.Answer[0])
|
||||
}
|
||||
if answer.Target != "srv.example.com." || answer.Port != 5222 {
|
||||
t.Fatalf("unexpected SRV answer: target=%s port=%d", answer.Target, answer.Port)
|
||||
}
|
||||
if upstreamQueries.Load() != 1 {
|
||||
t.Fatalf("expected SRV query to be forwarded once, got %d upstream queries", upstreamQueries.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeDNSForProxyRuleReturnsNoDataWhenAddressFamilyUnavailable(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1"}, "", "223.5.5.5", "", nil)
|
||||
r.BlockRule = suffixtree.NewNodeFromRules()
|
||||
r.DirectRule = suffixtree.NewNodeFromRules()
|
||||
r.ProxyRule = suffixtree.NewNodeFromRules("example.com.")
|
||||
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("example.com.", dns.TypeAAAA)
|
||||
writer := &mockDNSWriter{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 53}}
|
||||
|
||||
r.ServeDNS(writer, req)
|
||||
|
||||
if writer.msg == nil {
|
||||
t.Fatal("expected response message")
|
||||
}
|
||||
if writer.msg.Rcode != dns.RcodeSuccess {
|
||||
t.Fatalf("expected successful response, got rcode %d", writer.msg.Rcode)
|
||||
}
|
||||
if len(writer.msg.Answer) != 0 {
|
||||
t.Fatalf("expected NODATA response, got %d answers", len(writer.msg.Answer))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildUpstreamAddrsIsPerRouter(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r1 := NewRouter([]string{"127.0.0.1"}, "1.1.1.1", "9.9.9.9", "", nil)
|
||||
r2 := NewRouter([]string{"127.0.0.2"}, "8.8.8.8", "4.4.4.4", "", nil)
|
||||
|
||||
addrs1, err := r1.buildUpstreamAddrs()
|
||||
if err != nil {
|
||||
t.Fatalf("buildUpstreamAddrs r1 failed: %v", err)
|
||||
}
|
||||
addrs2, err := r2.buildUpstreamAddrs()
|
||||
if err != nil {
|
||||
t.Fatalf("buildUpstreamAddrs r2 failed: %v", err)
|
||||
}
|
||||
|
||||
if strings.Join(addrs1, ",") == strings.Join(addrs2, ",") {
|
||||
t.Fatalf("expected router-specific upstreams, got %v and %v", addrs1, addrs2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentUpstreamStateSchedulesPreferredProbeAfterRetryInterval(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1"}, "1.1.1.1", "9.9.9.9", "", nil)
|
||||
r.dns.upstreamAddrs = []string{"1.1.1.1:53", "9.9.9.9:53"}
|
||||
r.dns.upstreamIndex = 1
|
||||
r.dns.retryAt = time.Now().Add(-time.Second)
|
||||
|
||||
_, index, shouldProbe, err := r.currentUpstreamState(time.Now())
|
||||
if err != nil {
|
||||
t.Fatalf("currentUpstreamState failed: %v", err)
|
||||
}
|
||||
if index != 1 {
|
||||
t.Fatalf("expected current degraded upstream index 1, got %d", index)
|
||||
}
|
||||
if !shouldProbe {
|
||||
t.Fatal("expected preferred upstream probe to be scheduled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentUpstreamStateAllowsOnlyOnePreferredProbe(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Now()
|
||||
r := NewRouter([]string{"127.0.0.1"}, "1.1.1.1", "9.9.9.9", "", nil)
|
||||
r.dns.upstreamAddrs = []string{"1.1.1.1:53", "9.9.9.9:53"}
|
||||
r.dns.upstreamIndex = 1
|
||||
r.dns.retryAt = now.Add(-time.Second)
|
||||
|
||||
_, _, shouldProbe, err := r.currentUpstreamState(now)
|
||||
if err != nil {
|
||||
t.Fatalf("currentUpstreamState failed: %v", err)
|
||||
}
|
||||
if !shouldProbe {
|
||||
t.Fatal("expected first caller to probe preferred upstream")
|
||||
}
|
||||
|
||||
_, _, shouldProbe, err = r.currentUpstreamState(now)
|
||||
if err != nil {
|
||||
t.Fatalf("currentUpstreamState second call failed: %v", err)
|
||||
}
|
||||
if shouldProbe {
|
||||
t.Fatal("expected second caller to reuse in-flight probe state")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDegradeUpstreamMovesTowardFallback(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1"}, "1.1.1.1", "9.9.9.9", "", nil)
|
||||
r.dns.upstreamAddrs = []string{"1.1.1.1:53", "9.9.9.9:53"}
|
||||
r.dns.upstreamIndex = 0
|
||||
|
||||
index, switched := r.degradeUpstream(0)
|
||||
if !switched {
|
||||
t.Fatal("expected upstream switch")
|
||||
}
|
||||
if index != 1 {
|
||||
t.Fatalf("expected fallback index 1, got %d", index)
|
||||
}
|
||||
if r.dns.retryAt.IsZero() {
|
||||
t.Fatal("expected retry deadline to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromoteUpstreamRestoresPreferredAndClearsRetry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1"}, "1.1.1.1", "9.9.9.9", "", nil)
|
||||
r.dns.upstreamAddrs = []string{"1.1.1.1:53", "9.9.9.9:53"}
|
||||
r.dns.upstreamIndex = 1
|
||||
r.dns.retryAt = time.Now().Add(time.Minute)
|
||||
|
||||
r.promoteUpstream()
|
||||
|
||||
if r.dns.upstreamIndex != 0 {
|
||||
t.Fatalf("expected preferred index 0, got %d", r.dns.upstreamIndex)
|
||||
}
|
||||
if !r.dns.retryAt.IsZero() {
|
||||
t.Fatal("expected retry deadline to be cleared")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentUpstreamStateRefreshesDHCPUpstreams(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var calls atomic.Int32
|
||||
r := NewRouter([]string{"127.0.0.1"}, "", "9.9.9.9", "", nil)
|
||||
r.dns.getDNSServer = func() ([]string, error) {
|
||||
if calls.Add(1) == 1 {
|
||||
return []string{"1.1.1.1"}, nil
|
||||
}
|
||||
return []string{"8.8.8.8"}, nil
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
addrs, index, shouldProbe, err := r.currentUpstreamState(now)
|
||||
if err != nil {
|
||||
t.Fatalf("initial currentUpstreamState failed: %v", err)
|
||||
}
|
||||
if index != 0 || shouldProbe {
|
||||
t.Fatalf("unexpected initial state: index=%d shouldProbe=%v", index, shouldProbe)
|
||||
}
|
||||
if strings.Join(addrs, ",") != "1.1.1.1:53,9.9.9.9:53" {
|
||||
t.Fatalf("unexpected initial upstreams: %v", addrs)
|
||||
}
|
||||
|
||||
addrs, index, shouldProbe, err = r.currentUpstreamState(now.Add(dnsRefreshTTL + time.Second))
|
||||
if err != nil {
|
||||
t.Fatalf("refreshed currentUpstreamState failed: %v", err)
|
||||
}
|
||||
if index != 0 || shouldProbe {
|
||||
t.Fatalf("unexpected refreshed state: index=%d shouldProbe=%v", index, shouldProbe)
|
||||
}
|
||||
if strings.Join(addrs, ",") != "8.8.8.8:53,9.9.9.9:53" {
|
||||
t.Fatalf("unexpected refreshed upstreams: %v", addrs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentUpstreamStateBacksOffInitialDHCPFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var calls atomic.Int32
|
||||
r := NewRouter([]string{"127.0.0.1"}, "", "", "", nil)
|
||||
r.dns.getDNSServer = func() ([]string, error) {
|
||||
calls.Add(1)
|
||||
return nil, errors.New("dhcp unavailable")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if _, _, _, err := r.currentUpstreamState(now); err == nil {
|
||||
t.Fatal("expected initial DHCP failure")
|
||||
}
|
||||
if calls.Load() != 1 {
|
||||
t.Fatalf("expected one DHCP call, got %d", calls.Load())
|
||||
}
|
||||
|
||||
if _, _, _, err := r.currentUpstreamState(now.Add(time.Second)); err == nil {
|
||||
t.Fatal("expected cached DHCP failure during backoff")
|
||||
}
|
||||
if calls.Load() != 1 {
|
||||
t.Fatalf("expected no extra DHCP call during backoff, got %d", calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentUpstreamStateRefreshDoesNotBlockConcurrentReaders(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
startRefresh := make(chan struct{})
|
||||
releaseRefresh := make(chan struct{})
|
||||
var calls atomic.Int32
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1"}, "", "9.9.9.9", "", nil)
|
||||
r.dns.upstreamAddrs = []string{"1.1.1.1:53", "9.9.9.9:53"}
|
||||
r.dns.upstreamIndex = 0
|
||||
r.dns.refreshAt = time.Now().Add(-time.Second)
|
||||
r.dns.getDNSServer = func() ([]string, error) {
|
||||
if calls.Add(1) == 1 {
|
||||
close(startRefresh)
|
||||
<-releaseRefresh
|
||||
}
|
||||
return []string{"8.8.8.8"}, nil
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_, _, _, _ = r.currentUpstreamState(time.Now())
|
||||
}()
|
||||
|
||||
<-startRefresh
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
addrs, index, shouldProbe, err := r.currentUpstreamState(time.Now())
|
||||
if err != nil {
|
||||
t.Errorf("concurrent currentUpstreamState failed: %v", err)
|
||||
return
|
||||
}
|
||||
if index != 0 || shouldProbe {
|
||||
t.Errorf("unexpected concurrent state: index=%d shouldProbe=%v", index, shouldProbe)
|
||||
return
|
||||
}
|
||||
if strings.Join(addrs, ",") != "1.1.1.1:53,9.9.9.9:53" {
|
||||
t.Errorf("unexpected concurrent upstreams: %v", addrs)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
t.Fatal("concurrent reader blocked on refresh lock")
|
||||
}
|
||||
|
||||
close(releaseRefresh)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestCurrentUpstreamStateUsesFallbackWhileInitialRefreshInFlight(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
startRefresh := make(chan struct{})
|
||||
releaseRefresh := make(chan struct{})
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1"}, "", "9.9.9.9", "", nil)
|
||||
r.dns.getDNSServer = func() ([]string, error) {
|
||||
close(startRefresh)
|
||||
<-releaseRefresh
|
||||
return []string{"1.1.1.1"}, nil
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_, _, _, _ = r.currentUpstreamState(time.Now())
|
||||
}()
|
||||
|
||||
<-startRefresh
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
addrs, index, shouldProbe, err := r.currentUpstreamState(time.Now())
|
||||
if err != nil {
|
||||
t.Errorf("currentUpstreamState during initial refresh failed: %v", err)
|
||||
return
|
||||
}
|
||||
if index != 0 || shouldProbe {
|
||||
t.Errorf("unexpected fallback state: index=%d shouldProbe=%v", index, shouldProbe)
|
||||
return
|
||||
}
|
||||
if strings.Join(addrs, ",") != "9.9.9.9:53" {
|
||||
t.Errorf("expected fallback-only upstreams, got %v", addrs)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
t.Fatal("concurrent caller blocked instead of using fallback")
|
||||
}
|
||||
|
||||
close(releaseRefresh)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestCurrentUpstreamStatePreservesProbeWhenRefreshIsDue(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Now()
|
||||
r := NewRouter([]string{"127.0.0.1"}, "", "9.9.9.9", "", nil)
|
||||
r.dns.upstreamAddrs = []string{"1.1.1.1:53", "9.9.9.9:53"}
|
||||
r.dns.upstreamIndex = 1
|
||||
r.dns.retryAt = now.Add(-time.Second)
|
||||
r.dns.refreshAt = now.Add(-time.Second)
|
||||
r.dns.getDNSServer = func() ([]string, error) {
|
||||
return []string{"8.8.8.8"}, nil
|
||||
}
|
||||
|
||||
addrs, index, shouldProbe, err := r.currentUpstreamState(now)
|
||||
if err != nil {
|
||||
t.Fatalf("currentUpstreamState with refresh failed: %v", err)
|
||||
}
|
||||
if !shouldProbe {
|
||||
t.Fatal("expected refresh to preserve due preferred probe")
|
||||
}
|
||||
if index != 1 {
|
||||
t.Fatalf("expected degraded upstream to be preserved across refresh, got %d", index)
|
||||
}
|
||||
if strings.Join(addrs, ",") != "8.8.8.8:53,9.9.9.9:53" {
|
||||
t.Fatalf("unexpected refreshed upstreams: %v", addrs)
|
||||
}
|
||||
if !r.dns.probeInFlight {
|
||||
t.Fatal("expected probe to be marked in-flight after refresh")
|
||||
}
|
||||
if !r.dns.retryAt.After(now) {
|
||||
t.Fatalf("expected retryAt to be pushed forward after scheduling probe, got %v", r.dns.retryAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeDNSReturnsNXDomainForBlockedDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1"}, "", "223.5.5.5", "", nil)
|
||||
r.BlockRule = suffixtree.NewNodeFromRules("example.com.")
|
||||
r.DirectRule = suffixtree.NewNodeFromRules()
|
||||
r.ProxyRule = suffixtree.NewNodeFromRules()
|
||||
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("example.com.", dns.TypeA)
|
||||
|
||||
writer := &mockDNSWriter{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 53}}
|
||||
r.ServeDNS(writer, req)
|
||||
|
||||
if writer.msg == nil {
|
||||
t.Fatal("expected response message")
|
||||
}
|
||||
if writer.msg.Rcode != dns.RcodeNameError {
|
||||
t.Fatalf("expected NXDOMAIN, got %d", writer.msg.Rcode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeDNSRejectsMultipleQuestions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1"}, "", "223.5.5.5", "", nil)
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("example.com.", dns.TypeA)
|
||||
req.Question = append(req.Question, dns.Question{Name: "example.org.", Qtype: dns.TypeA, Qclass: dns.ClassINET})
|
||||
|
||||
writer := &mockDNSWriter{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 53}}
|
||||
r.ServeDNS(writer, req)
|
||||
|
||||
if writer.msg == nil {
|
||||
t.Fatal("expected response message")
|
||||
}
|
||||
if writer.msg.Rcode != dns.RcodeFormatError {
|
||||
t.Fatalf("expected format error, got %d", writer.msg.Rcode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExchangeWithRetryFallsBackToTCPOnTruncatedUDP(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
udpConn, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen udp: %v", err)
|
||||
}
|
||||
defer udpConn.Close()
|
||||
|
||||
tcpLn, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strings.Split(udpConn.LocalAddr().String(), ":")[1]))
|
||||
if err != nil {
|
||||
t.Fatalf("listen tcp: %v", err)
|
||||
}
|
||||
defer tcpLn.Close()
|
||||
|
||||
var tcpQueries atomic.Int32
|
||||
handler := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
resp := new(dns.Msg)
|
||||
resp.SetReply(req)
|
||||
if _, ok := w.RemoteAddr().(*net.TCPAddr); ok {
|
||||
tcpQueries.Add(1)
|
||||
resp.Answer = []dns.RR{&dns.A{
|
||||
Hdr: dns.RR_Header{Name: req.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 20},
|
||||
A: net.ParseIP("127.0.0.1"),
|
||||
}}
|
||||
} else {
|
||||
resp.Truncated = true
|
||||
}
|
||||
_ = w.WriteMsg(resp)
|
||||
})
|
||||
|
||||
udpServer := &dns.Server{PacketConn: udpConn, Handler: handler}
|
||||
tcpServer := &dns.Server{Listener: tcpLn, Handler: handler}
|
||||
go func() { _ = udpServer.ActivateAndServe() }()
|
||||
go func() { _ = tcpServer.ActivateAndServe() }()
|
||||
t.Cleanup(func() {
|
||||
_ = udpServer.Shutdown()
|
||||
_ = tcpServer.Shutdown()
|
||||
})
|
||||
|
||||
r := NewRouter(nil, "", "223.5.5.5", "", nil)
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("example.com.", dns.TypeA)
|
||||
|
||||
resp, err := r.exchangeWithRetry(req, udpConn.LocalAddr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("exchangeWithRetry failed: %v", err)
|
||||
}
|
||||
if len(resp.Answer) != 1 {
|
||||
t.Fatalf("expected tcp answer, got %d answers", len(resp.Answer))
|
||||
}
|
||||
if tcpQueries.Load() == 0 {
|
||||
t.Fatal("expected TCP fallback query")
|
||||
}
|
||||
}
|
||||
|
||||
type mockDNSWriter struct {
|
||||
msg *dns.Msg
|
||||
localAddr net.Addr
|
||||
}
|
||||
|
||||
func (w *mockDNSWriter) LocalAddr() net.Addr { return w.localAddr }
|
||||
func (w *mockDNSWriter) RemoteAddr() net.Addr { return &net.UDPAddr{} }
|
||||
func (w *mockDNSWriter) WriteMsg(msg *dns.Msg) error { w.msg = msg; return nil }
|
||||
func (w *mockDNSWriter) Write([]byte) (int, error) { return 0, nil }
|
||||
func (w *mockDNSWriter) Close() error { return nil }
|
||||
func (w *mockDNSWriter) TsigStatus() error { return nil }
|
||||
func (w *mockDNSWriter) TsigTimersOnly(bool) {}
|
||||
func (w *mockDNSWriter) Hijack() {}
|
||||
@@ -0,0 +1,28 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func ParseHostPort(hostport string, u *url.URL) (string, uint16, error) {
|
||||
host, port, err := net.SplitHostPort(hostport)
|
||||
if err != nil {
|
||||
if err.(*net.AddrError).Err == "missing port in address" {
|
||||
switch u.Scheme {
|
||||
case "http":
|
||||
return hostport, 80, nil
|
||||
case "https":
|
||||
return hostport, 443, nil
|
||||
}
|
||||
}
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
portInt, err := strconv.ParseUint(port, 10, 16)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
return host, uint16(portInt), nil
|
||||
}
|
||||
+16
-4
@@ -17,7 +17,7 @@ type authReq struct {
|
||||
|
||||
func (req *authReq) Fulfill(r io.Reader) error {
|
||||
buf := make([]byte, 2)
|
||||
if n, err := r.Read(buf); err != nil || n != 2 {
|
||||
if n, err := io.ReadFull(r, buf); err != nil || n != 2 {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func (req *authReq) Fulfill(r io.Reader) error {
|
||||
req.NMETHODS = buf[1]
|
||||
|
||||
req.METHODS = make([]byte, int(req.NMETHODS))
|
||||
if n, err := r.Read(req.METHODS); err != nil || n != len(req.METHODS) {
|
||||
if n, err := io.ReadFull(r, req.METHODS); err != nil || n != len(req.METHODS) {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -33,7 +33,15 @@ func (req *authReq) Fulfill(r io.Reader) error {
|
||||
}
|
||||
|
||||
func (r *authReq) IsValid() bool {
|
||||
return r.VER == 5 && r.METHODS[0] == 0
|
||||
if r.VER != 5 || len(r.METHODS) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, method := range r.METHODS {
|
||||
if method == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 2. server response auth request
|
||||
@@ -51,7 +59,7 @@ type reqHead struct {
|
||||
}
|
||||
|
||||
func (r *reqHead) IsValid() bool {
|
||||
return r.VER == 5 && r.CMD == 1
|
||||
return r.VER == 5 && r.CMD == 1 && r.RSV == 0
|
||||
}
|
||||
|
||||
// 4. server response with the address that assigned to connect to target address
|
||||
@@ -70,6 +78,7 @@ type addrType interface {
|
||||
}
|
||||
|
||||
// ATYP:
|
||||
//
|
||||
// 0x01 -> net.IPv4len
|
||||
// 0x03 -> first byte is length
|
||||
// 0x04 -> net.IPv6len
|
||||
@@ -81,6 +90,7 @@ type addrTypeIPv4 struct {
|
||||
func (a *addrTypeIPv4) Fulfill(r io.Reader) error {
|
||||
return binary.Read(r, binary.BigEndian, a)
|
||||
}
|
||||
|
||||
func (a *addrTypeIPv4) Addr() (string, uint16) {
|
||||
return net.IP(a.DST_ADDR[:]).String(), a.DST_PORT
|
||||
}
|
||||
@@ -108,6 +118,7 @@ func (a *addrTypeDomain) Fulfill(r io.Reader) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *addrTypeDomain) Addr() (string, uint16) {
|
||||
return string(a.DST_ADDR[:]), a.DST_PORT
|
||||
}
|
||||
@@ -120,6 +131,7 @@ type addrTypeIPv6 struct {
|
||||
func (a *addrTypeIPv6) Fulfill(r io.Reader) error {
|
||||
return binary.Read(r, binary.BigEndian, a)
|
||||
}
|
||||
|
||||
func (a *addrTypeIPv6) Addr() (string, uint16) {
|
||||
return net.IP(a.DST_ADDR[:]).String(), a.DST_PORT
|
||||
}
|
||||
|
||||
+59
-12
@@ -28,14 +28,32 @@ func New() *Socks5 {
|
||||
return &Socks5{}
|
||||
}
|
||||
|
||||
var noAuthResp = authResp{VER: 5, METHOD: 0}
|
||||
var succHeadResp = respHead{VER: 5, REP: 0, RSV: 0, ATYP: 1}
|
||||
const (
|
||||
RepSucceeded = 0x00
|
||||
RepGeneralFailure = 0x01
|
||||
RepConnectionNotAllowed = 0x02
|
||||
)
|
||||
|
||||
var (
|
||||
noAuthResp = authResp{VER: 5, METHOD: 0}
|
||||
)
|
||||
|
||||
func (s *Socks5) Unwrap(conn net.Conn) (net.Addr, error) {
|
||||
{ //auth
|
||||
addr, err := s.ReadRequest(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.WriteReply(conn, RepSucceeded); err != nil {
|
||||
return nil, errors.Wrap(err, "write head")
|
||||
}
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
func (s *Socks5) ReadRequest(conn net.Conn) (net.Addr, error) {
|
||||
{ // auth
|
||||
auth := new(authReq)
|
||||
if err := auth.Fulfill(conn); err != nil && !auth.IsValid() {
|
||||
return nil, errors.Errorf("read head: %v, err: %s", auth, err)
|
||||
if err := auth.Fulfill(conn); err != nil || !auth.IsValid() {
|
||||
return nil, errors.Errorf("read auth head: %v, err: %s", auth, err)
|
||||
}
|
||||
|
||||
if err := binary.Write(conn, binary.BigEndian, noAuthResp); err != nil {
|
||||
@@ -63,10 +81,6 @@ func (s *Socks5) Unwrap(conn net.Conn) (net.Addr, error) {
|
||||
if err := addr.Fulfill(conn); err != nil {
|
||||
return nil, errors.Wrap(err, "read target")
|
||||
}
|
||||
|
||||
if err := binary.Write(conn, binary.BigEndian, succHeadResp); err != nil {
|
||||
return nil, errors.Wrap(err, "write head")
|
||||
}
|
||||
}
|
||||
|
||||
return &AddrHead{
|
||||
@@ -74,6 +88,11 @@ func (s *Socks5) Unwrap(conn net.Conn) (net.Addr, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Socks5) WriteReply(conn net.Conn, rep byte) error {
|
||||
head := respHead{VER: 5, REP: rep, RSV: 0, ATYP: 1}
|
||||
return binary.Write(conn, binary.BigEndian, head)
|
||||
}
|
||||
|
||||
var noAuthReq = struct {
|
||||
VER byte
|
||||
NMETHODS uint8
|
||||
@@ -82,6 +101,10 @@ var noAuthReq = struct {
|
||||
var domainHead = reqHead{VER: 5, CMD: 1, RSV: 0, ATYP: 3}
|
||||
|
||||
func (s *Socks5) Wrap(conn net.Conn, tgtHost string, tgtPort uint16) error {
|
||||
if len(tgtHost) > 255 {
|
||||
return errors.Errorf("target host too long: %d", len(tgtHost))
|
||||
}
|
||||
|
||||
{ // auth
|
||||
if err := binary.Write(conn, binary.BigEndian, &noAuthReq); err != nil {
|
||||
return errors.WithStack(err)
|
||||
@@ -91,6 +114,9 @@ func (s *Socks5) Wrap(conn net.Conn, tgtHost string, tgtPort uint16) error {
|
||||
if err := binary.Read(conn, binary.BigEndian, resp); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if resp.VER != 5 || resp.METHOD != 0 {
|
||||
return errors.Errorf("unexpected auth response: %+v", resp)
|
||||
}
|
||||
}
|
||||
{ // head
|
||||
buf := bytes.NewBuffer(make([]byte, 0, binary.Size(domainHead)+1+len(tgtHost)+2))
|
||||
@@ -99,14 +125,35 @@ func (s *Socks5) Wrap(conn net.Conn, tgtHost string, tgtPort uint16) error {
|
||||
buf.WriteString(tgtHost)
|
||||
buf.Write([]byte{byte(tgtPort >> 8), byte(tgtPort)})
|
||||
|
||||
if _, err := conn.Write(buf.Bytes()); err != nil {
|
||||
return errors.WithStack(err)
|
||||
if n, err := conn.Write(buf.Bytes()); err != nil || n != len(buf.Bytes()) {
|
||||
return errors.Errorf("n: %d, err: %s", n, err)
|
||||
}
|
||||
|
||||
head := respHead{}
|
||||
head := reqHead{}
|
||||
if err := binary.Read(conn, binary.BigEndian, &head); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if head.VER != 5 || head.RSV != 0 {
|
||||
return errors.Errorf("unexpected response head: %+v", head)
|
||||
}
|
||||
if head.CMD != 0 {
|
||||
return errors.Errorf("connect rejected, rep=%d", head.CMD)
|
||||
}
|
||||
|
||||
var addr addrType
|
||||
switch head.ATYP {
|
||||
case 0x01:
|
||||
addr = &addrTypeIPv4{}
|
||||
case 0x03:
|
||||
addr = &addrTypeDomain{}
|
||||
case 0x04:
|
||||
addr = &addrTypeIPv6{}
|
||||
default:
|
||||
return errors.Errorf("invalid response ATYP: %d", head.ATYP)
|
||||
}
|
||||
if err := addr.Fulfill(conn); err != nil {
|
||||
return errors.Wrap(err, "read response address")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package socks5
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type mockConn struct {
|
||||
reader io.Reader
|
||||
writes bytes.Buffer
|
||||
}
|
||||
|
||||
func newMockConn(data []byte) *mockConn {
|
||||
return &mockConn{reader: bytes.NewReader(data)}
|
||||
}
|
||||
|
||||
func (c *mockConn) Read(p []byte) (int, error) { return c.reader.Read(p) }
|
||||
func (c *mockConn) Write(p []byte) (int, error) { return c.writes.Write(p) }
|
||||
func (c *mockConn) Close() error { return nil }
|
||||
func (c *mockConn) LocalAddr() net.Addr { return dummyAddr("local") }
|
||||
func (c *mockConn) RemoteAddr() net.Addr { return dummyAddr("remote") }
|
||||
func (c *mockConn) SetDeadline(time.Time) error { return nil }
|
||||
func (c *mockConn) SetReadDeadline(time.Time) error { return nil }
|
||||
func (c *mockConn) SetWriteDeadline(time.Time) error { return nil }
|
||||
|
||||
type dummyAddr string
|
||||
|
||||
func (a dummyAddr) Network() string { return "tcp" }
|
||||
func (a dummyAddr) String() string { return string(a) }
|
||||
|
||||
func TestUnwrapRejectsZeroMethodsWithoutPanic(t *testing.T) {
|
||||
conn := newMockConn([]byte{0x05, 0x00})
|
||||
if _, err := New().Unwrap(conn); err == nil {
|
||||
t.Fatal("expected error for zero auth methods")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnwrapAcceptsNoAuthWhenNotFirstMethod(t *testing.T) {
|
||||
req := []byte{
|
||||
0x05, 0x02, 0x02, 0x00,
|
||||
0x05, 0x01, 0x00, 0x03,
|
||||
0x0b,
|
||||
}
|
||||
req = append(req, []byte("example.com")...)
|
||||
req = append(req, 0x01, 0xbb)
|
||||
|
||||
addr, err := New().Unwrap(newMockConn(req))
|
||||
if err != nil {
|
||||
t.Fatalf("unwrap failed: %v", err)
|
||||
}
|
||||
|
||||
host, port := addr.(*AddrHead).Addr()
|
||||
if host != "example.com" || port != 443 {
|
||||
t.Fatalf("unexpected addr: %s:%d", host, port)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapRejectsAuthFailure(t *testing.T) {
|
||||
server, client := net.Pipe()
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
buf := make([]byte, 3)
|
||||
if _, err := io.ReadFull(server, buf); err != nil {
|
||||
return
|
||||
}
|
||||
_, _ = server.Write([]byte{0x05, 0xff})
|
||||
}()
|
||||
|
||||
if err := New().Wrap(client, "example.com", 443); err == nil {
|
||||
t.Fatal("expected auth failure")
|
||||
}
|
||||
<-done
|
||||
}
|
||||
|
||||
func TestWrapRejectsConnectFailure(t *testing.T) {
|
||||
server, client := net.Pipe()
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
|
||||
auth := make([]byte, 3)
|
||||
if _, err := io.ReadFull(server, auth); err != nil {
|
||||
return
|
||||
}
|
||||
_, _ = server.Write([]byte{0x05, 0x00})
|
||||
|
||||
if _, err := io.ReadFull(server, make([]byte, 4)); err != nil {
|
||||
return
|
||||
}
|
||||
var nameLen [1]byte
|
||||
if _, err := io.ReadFull(server, nameLen[:]); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err := io.ReadFull(server, make([]byte, int(nameLen[0])+2)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp := bytes.NewBuffer(nil)
|
||||
_ = binary.Write(resp, binary.BigEndian, reqHead{VER: 5, CMD: 5, RSV: 0, ATYP: 1})
|
||||
_, _ = server.Write(resp.Bytes())
|
||||
}()
|
||||
|
||||
if err := New().Wrap(client, "example.com", 443); err == nil {
|
||||
t.Fatal("expected connect failure")
|
||||
}
|
||||
<-done
|
||||
}
|
||||
|
||||
func TestWrapRejectsTooLongHost(t *testing.T) {
|
||||
if err := New().Wrap(newMockConn(nil), string(bytes.Repeat([]byte{'a'}, 256)), 443); err == nil {
|
||||
t.Fatal("expected error for too long host")
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
@@ -26,7 +27,10 @@ type Head struct {
|
||||
|
||||
func (h *Head) Network() string { return "tcp" }
|
||||
func (h *Head) String() string {
|
||||
idx := bytes.IndexRune(h.TgtAddr[:], 0)
|
||||
idx := bytes.IndexByte(h.TgtAddr[:], 0)
|
||||
if idx < 0 {
|
||||
idx = len(h.TgtAddr)
|
||||
}
|
||||
addr := string(h.TgtAddr[:idx])
|
||||
return net.JoinHostPort(addr, strconv.Itoa(int(h.Port)))
|
||||
}
|
||||
@@ -43,7 +47,7 @@ func New(password string) *Sower {
|
||||
|
||||
func (s *Sower) Unwrap(conn net.Conn) (net.Addr, error) {
|
||||
buf := make([]byte, headSize)
|
||||
if n, err := conn.Read(buf); err != nil || n != headSize {
|
||||
if n, err := io.ReadFull(conn, buf); err != nil || n != headSize {
|
||||
return nil, errors.Errorf("n: %d, err: %s", n, err)
|
||||
}
|
||||
|
||||
@@ -63,6 +67,10 @@ func (s *Sower) Unwrap(conn net.Conn) (net.Addr, error) {
|
||||
}
|
||||
|
||||
func (s *Sower) Wrap(conn net.Conn, tgtHost string, tgtPort uint16) error {
|
||||
if len(tgtHost) > maxDomainLength {
|
||||
return errors.Errorf("target host too long: %d", len(tgtHost))
|
||||
}
|
||||
|
||||
tgtAddr := [maxDomainLength]byte{}
|
||||
copy(tgtAddr[:len(tgtHost)], []byte(tgtHost))
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package sower
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type chunkConn struct {
|
||||
reader io.Reader
|
||||
writes bytes.Buffer
|
||||
}
|
||||
|
||||
func newChunkConn(data []byte, chunkSize int) *chunkConn {
|
||||
return &chunkConn{
|
||||
reader: &io.LimitedReader{
|
||||
R: &chunkReader{
|
||||
data: data,
|
||||
chunk: chunkSize,
|
||||
},
|
||||
N: int64(len(data)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *chunkConn) Read(p []byte) (int, error) { return c.reader.Read(p) }
|
||||
func (c *chunkConn) Write(p []byte) (int, error) { return c.writes.Write(p) }
|
||||
func (c *chunkConn) Close() error { return nil }
|
||||
func (c *chunkConn) LocalAddr() net.Addr { return dummyAddr("local") }
|
||||
func (c *chunkConn) RemoteAddr() net.Addr { return dummyAddr("remote") }
|
||||
func (c *chunkConn) SetDeadline(time.Time) error { return nil }
|
||||
func (c *chunkConn) SetReadDeadline(time.Time) error { return nil }
|
||||
func (c *chunkConn) SetWriteDeadline(time.Time) error { return nil }
|
||||
|
||||
type chunkReader struct {
|
||||
data []byte
|
||||
chunk int
|
||||
pos int
|
||||
}
|
||||
|
||||
func (r *chunkReader) Read(p []byte) (int, error) {
|
||||
if r.pos >= len(r.data) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
n := r.chunk
|
||||
if n > len(p) {
|
||||
n = len(p)
|
||||
}
|
||||
remain := len(r.data) - r.pos
|
||||
if n > remain {
|
||||
n = remain
|
||||
}
|
||||
copy(p, r.data[r.pos:r.pos+n])
|
||||
r.pos += n
|
||||
return n, nil
|
||||
}
|
||||
|
||||
type dummyAddr string
|
||||
|
||||
func (a dummyAddr) Network() string { return "tcp" }
|
||||
func (a dummyAddr) String() string { return string(a) }
|
||||
|
||||
func TestWrapRejectsTooLongHost(t *testing.T) {
|
||||
err := New("123").Wrap(newChunkConn(nil, 1), strings.Repeat("a", maxDomainLength+1), 443)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for too long host")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundTripWithMaxLengthHost(t *testing.T) {
|
||||
host := strings.Repeat("a", maxDomainLength)
|
||||
server, client := net.Pipe()
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- New("123").Wrap(client, host, 443)
|
||||
}()
|
||||
|
||||
addr, err := New("123").Unwrap(server)
|
||||
if err != nil {
|
||||
t.Fatalf("unwrap failed: %v", err)
|
||||
}
|
||||
if got := addr.String(); got != net.JoinHostPort(host, "443") {
|
||||
t.Fatalf("unexpected addr: %s", got)
|
||||
}
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("wrap failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnwrapReadsFullHeader(t *testing.T) {
|
||||
target := [maxDomainLength]byte{}
|
||||
copy(target[:], "example.com")
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
if err := binary.Write(buf, binary.BigEndian, &Head{
|
||||
Cmd: 0x80,
|
||||
Checksum: sumChecksum(target, []byte("123")),
|
||||
Port: 443,
|
||||
TgtAddr: target,
|
||||
}); err != nil {
|
||||
t.Fatalf("build header: %v", err)
|
||||
}
|
||||
|
||||
addr, err := New("123").Unwrap(newChunkConn(buf.Bytes(), 7))
|
||||
if err != nil {
|
||||
t.Fatalf("unwrap failed: %v", err)
|
||||
}
|
||||
if got := addr.String(); got != "example.com:443" {
|
||||
t.Fatalf("unexpected addr: %s", got)
|
||||
}
|
||||
}
|
||||
@@ -5,16 +5,11 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/sower-proxy/conns/teeconn"
|
||||
"github.com/sower-proxy/deferlog/log"
|
||||
"github.com/wweir/sower/transport/sower"
|
||||
"github.com/wweir/sower/transport/trojan"
|
||||
"github.com/sower-proxy/conns/reread"
|
||||
"github.com/sower-proxy/sower/transport/sower"
|
||||
"github.com/sower-proxy/sower/transport/trojan"
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.Logger = log.Logger.With().Caller().Logger()
|
||||
}
|
||||
|
||||
func testPipe(tran Transport) (net.Addr, error) {
|
||||
r, w := net.Pipe()
|
||||
defer r.Close()
|
||||
@@ -24,7 +19,7 @@ func testPipe(tran Transport) (net.Addr, error) {
|
||||
tran.Wrap(w, "sower", 443)
|
||||
}(w)
|
||||
|
||||
return tran.Unwrap(teeconn.New(r))
|
||||
return tran.Unwrap(reread.New(r))
|
||||
}
|
||||
|
||||
func Test_Transports(t *testing.T) {
|
||||
|
||||
+49
-11
@@ -50,6 +50,10 @@ func (a *ipv4Addr) String() string {
|
||||
return net.JoinHostPort(net.IP(a.ADDR[:]).String(), strconv.Itoa(int(a.PORT)))
|
||||
}
|
||||
|
||||
func (a *ipv4Addr) IsValid() bool {
|
||||
return a.CRLF == [2]byte{0x0D, 0x0A}
|
||||
}
|
||||
|
||||
type ipv6Addr struct {
|
||||
ADDR [16]byte
|
||||
PORT uint16
|
||||
@@ -61,6 +65,10 @@ func (a *ipv6Addr) String() string {
|
||||
return net.JoinHostPort(net.IP(a.ADDR[:]).String(), strconv.Itoa(int(a.PORT)))
|
||||
}
|
||||
|
||||
func (a *ipv6Addr) IsValid() bool {
|
||||
return a.CRLF == [2]byte{0x0D, 0x0A}
|
||||
}
|
||||
|
||||
type domain struct {
|
||||
ADDR string
|
||||
PORT uint16
|
||||
@@ -71,23 +79,29 @@ func (*domain) Network() string { return "tcp" }
|
||||
func (a *domain) String() string {
|
||||
return net.JoinHostPort(a.ADDR, strconv.Itoa(int(a.PORT)))
|
||||
}
|
||||
|
||||
func (a *domain) Fulfill(r io.Reader) error {
|
||||
buf := make([]byte, 1)
|
||||
if n, err := r.Read(buf); err != nil || n != 1 {
|
||||
if n, err := io.ReadFull(r, buf); err != nil || n != 1 {
|
||||
return errors.New("read domain length failed")
|
||||
}
|
||||
|
||||
addrLen := int(buf[0])
|
||||
buf = make([]byte, addrLen+4)
|
||||
if n, err := r.Read(buf); err != nil || n != addrLen+4 {
|
||||
if n, err := io.ReadFull(r, buf); err != nil || n != addrLen+4 {
|
||||
return errors.Wrap(err, "read doamin failed")
|
||||
}
|
||||
|
||||
a.ADDR = string(buf[:addrLen])
|
||||
a.PORT = uint16(buf[addrLen])<<8 + uint16(buf[addrLen+1])
|
||||
a.CRLF = [2]byte{buf[addrLen+2], buf[addrLen+3]}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *domain) IsValid() bool {
|
||||
return a.CRLF == [2]byte{0x0D, 0x0A}
|
||||
}
|
||||
|
||||
type Trojan struct {
|
||||
headPasswd []byte
|
||||
|
||||
@@ -111,8 +125,7 @@ func New(password string) *Trojan {
|
||||
|
||||
func (t *Trojan) Unwrap(conn net.Conn) (net.Addr, error) {
|
||||
buf := make([]byte, headLen)
|
||||
// do not use io.ReadFull to avoid hang
|
||||
if n, err := conn.Read(buf); err != nil || n != headLen {
|
||||
if n, err := io.ReadFull(conn, buf); err != nil || n != headLen {
|
||||
return nil, errors.Errorf("n: %d, err: %s", n, err)
|
||||
}
|
||||
|
||||
@@ -124,23 +137,45 @@ func (t *Trojan) Unwrap(conn net.Conn) (net.Addr, error) {
|
||||
if !bytes.Equal(head.Passwd[:], []byte(t.headPasswd)) {
|
||||
return nil, errors.New("auth fail")
|
||||
}
|
||||
|
||||
head.CMD, head.ATYP = buf[58], buf[59]
|
||||
if head.CRLF != [2]byte{0x0D, 0x0A} {
|
||||
return nil, errors.New("invalid CRLF")
|
||||
}
|
||||
if head.CMD != 0x01 {
|
||||
return nil, errors.Errorf("invalid CMD: %d", head.CMD)
|
||||
}
|
||||
switch head.ATYP {
|
||||
case 0x01: //ipv4
|
||||
case 0x01: // ipv4
|
||||
addr := &ipv4Addr{}
|
||||
err := binary.Read(conn, binary.BigEndian, addr)
|
||||
return addr, errors.Wrap(err, "read addr")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "read addr")
|
||||
}
|
||||
if !addr.IsValid() {
|
||||
return nil, errors.New("invalid CRLF")
|
||||
}
|
||||
return addr, nil
|
||||
|
||||
case 0x04: //ipv6
|
||||
case 0x04: // ipv6
|
||||
addr := &ipv6Addr{}
|
||||
err := binary.Read(conn, binary.BigEndian, addr)
|
||||
return addr, errors.Wrap(err, "read addr")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "read addr")
|
||||
}
|
||||
if !addr.IsValid() {
|
||||
return nil, errors.New("invalid CRLF")
|
||||
}
|
||||
return addr, nil
|
||||
|
||||
case 0x03: // domain
|
||||
addr := &domain{}
|
||||
err := addr.Fulfill(conn)
|
||||
return addr, errors.Wrap(err, "read addr")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "read addr")
|
||||
}
|
||||
if !addr.IsValid() {
|
||||
return nil, errors.New("invalid CRLF")
|
||||
}
|
||||
return addr, nil
|
||||
|
||||
default:
|
||||
return nil, errors.New("invalid ATYP")
|
||||
@@ -160,6 +195,9 @@ func (t *Trojan) Wrap(conn net.Conn, tgtHost string, tgtPort uint16) error {
|
||||
buf.Write([]byte(ip.To16()))
|
||||
|
||||
default:
|
||||
if len(tgtHost) > 255 {
|
||||
return errors.Errorf("target host too long: %d", len(tgtHost))
|
||||
}
|
||||
buf.Write(t.headDomain)
|
||||
buf.WriteByte(byte(len(tgtHost)))
|
||||
buf.WriteString(tgtHost)
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package trojan
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type chunkConn struct {
|
||||
reader io.Reader
|
||||
writes bytes.Buffer
|
||||
}
|
||||
|
||||
func newChunkConn(data []byte, chunkSize int) *chunkConn {
|
||||
return &chunkConn{
|
||||
reader: &chunkReader{
|
||||
data: data,
|
||||
chunk: chunkSize,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *chunkConn) Read(p []byte) (int, error) { return c.reader.Read(p) }
|
||||
func (c *chunkConn) Write(p []byte) (int, error) { return c.writes.Write(p) }
|
||||
func (c *chunkConn) Close() error { return nil }
|
||||
func (c *chunkConn) LocalAddr() net.Addr { return dummyAddr("local") }
|
||||
func (c *chunkConn) RemoteAddr() net.Addr { return dummyAddr("remote") }
|
||||
func (c *chunkConn) SetDeadline(time.Time) error { return nil }
|
||||
func (c *chunkConn) SetReadDeadline(time.Time) error { return nil }
|
||||
func (c *chunkConn) SetWriteDeadline(time.Time) error { return nil }
|
||||
|
||||
type chunkReader struct {
|
||||
data []byte
|
||||
chunk int
|
||||
pos int
|
||||
}
|
||||
|
||||
func (r *chunkReader) Read(p []byte) (int, error) {
|
||||
if r.pos >= len(r.data) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
n := r.chunk
|
||||
if n > len(p) {
|
||||
n = len(p)
|
||||
}
|
||||
remain := len(r.data) - r.pos
|
||||
if n > remain {
|
||||
n = remain
|
||||
}
|
||||
copy(p, r.data[r.pos:r.pos+n])
|
||||
r.pos += n
|
||||
return n, nil
|
||||
}
|
||||
|
||||
type dummyAddr string
|
||||
|
||||
func (a dummyAddr) Network() string { return "tcp" }
|
||||
func (a dummyAddr) String() string { return string(a) }
|
||||
|
||||
func TestWrapRejectsTooLongHost(t *testing.T) {
|
||||
err := New("123").Wrap(newChunkConn(nil, 1), strings.Repeat("a", 256), 443)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for too long host")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnwrapReadsFullHeader(t *testing.T) {
|
||||
conn := newChunkConn(nil, 1)
|
||||
if err := New("123").Wrap(conn, "example.com", 443); err != nil {
|
||||
t.Fatalf("wrap failed: %v", err)
|
||||
}
|
||||
|
||||
addr, err := New("123").Unwrap(newChunkConn(conn.writes.Bytes(), 5))
|
||||
if err != nil {
|
||||
t.Fatalf("unwrap failed: %v", err)
|
||||
}
|
||||
if got := addr.String(); got != "example.com:443" {
|
||||
t.Fatalf("unexpected addr: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnwrapRejectsInvalidCommand(t *testing.T) {
|
||||
conn := newChunkConn(nil, 1)
|
||||
if err := New("123").Wrap(conn, "example.com", 443); err != nil {
|
||||
t.Fatalf("wrap failed: %v", err)
|
||||
}
|
||||
|
||||
data := append([]byte(nil), conn.writes.Bytes()...)
|
||||
data[58] = 0x03
|
||||
|
||||
if _, err := New("123").Unwrap(newChunkConn(data, len(data))); err == nil {
|
||||
t.Fatal("expected invalid command error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnwrapRejectsInvalidCRLF(t *testing.T) {
|
||||
conn := newChunkConn(nil, 1)
|
||||
if err := New("123").Wrap(conn, "example.com", 443); err != nil {
|
||||
t.Fatalf("wrap failed: %v", err)
|
||||
}
|
||||
|
||||
data := append([]byte(nil), conn.writes.Bytes()...)
|
||||
data[len(data)-1] = 0x00
|
||||
|
||||
if _, err := New("123").Unwrap(newChunkConn(data, len(data))); err == nil {
|
||||
t.Fatal("expected invalid CRLF error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user