Compare commits

...
40 Commits
Author SHA1 Message Date
wweir 8bbf5ca03d refactor: simplify dns proxy routing checks 2026-03-30 23:12:04 +08:00
wweir c3ef6ed88f refactor: merge upstream tls proxy branches 2026-03-30 23:11:58 +08:00
wweir 0c8bdf6d9c refactor: share upstream tls dial setup 2026-03-30 23:04:21 +08:00
wweir 53c55aef24 refactor: simplify dns proxy replies 2026-03-30 23:03:49 +08:00
wweir e705dd4dd3 docs: refresh agent collaboration guidelines 2026-03-30 23:03:17 +08:00
wweir 6d410b104b docs: add architecture and deployment notes 2026-03-30 23:03:07 +08:00
wweir 6ccdcc131b feat: add sowerd install and startup validation 2026-03-30 23:02:54 +08:00
wweir 2b072584e6 feat: add upstream tls controls for sower 2026-03-30 23:02:38 +08:00
wweir b2228f8643 refactor: harden routing and transport handling 2026-03-30 23:02:13 +08:00
温习江湖 5ab6acd7c7 Merge pull request #33 from sower-proxy/dependabot/go_modules/golang.org/x/crypto-0.45.0
chore(deps): bump golang.org/x/crypto from 0.37.0 to 0.45.0
2026-01-09 14:28:19 +08:00
dependabot[bot] a74b1204c0 chore(deps): bump golang.org/x/crypto from 0.37.0 to 0.45.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.37.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.37.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-20 02:29:16 +00:00
wweir 677b0593b8 refactor: update sowerd main.go configuration handling 2025-10-28 20:04:27 +08:00
wweir af61d38b9e refactor: extract configuration to dedicated config package 2025-10-27 11:28:42 +08:00
wweir f7a288af4c refactor: extract configuration to dedicated config package
- Move configuration structures from cmd/sower/main.go and cmd/sowerd/main.go to config package
- Add config.SowerConfig and config.SowerdConfig types
- Implement Validate() method for configuration validation
- Update Go module dependencies to latest versions
- Improve code organization and maintainability
2025-09-07 00:10:56 +08:00
wweir 91b7f8e8c1 refactor: rename module from github.com/wweir/sower to github.com/sower-proxy/sower
- Update go.mod module declaration
- Update all import paths in Go files
- Update GitHub links in README.md
- All tests pass and build succeeds
2025-09-06 08:27:11 +08:00
wweir 53b1de41d0 Update sower.service to use -c option for config file 2025-08-16 11:03:31 +08:00
温习江湖 6c9b9d9597 Merge pull request #32 from sower-proxy/dependabot/go_modules/golang.org/x/net-0.38.0
chore(deps): bump golang.org/x/net from 0.35.0 to 0.38.0
2025-07-12 06:20:08 +08:00
dependabot[bot] 1d8f0fdabe chore(deps): bump golang.org/x/net from 0.35.0 to 0.38.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.35.0 to 0.38.0.
- [Commits](https://github.com/golang/net/compare/v0.35.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.38.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-26 11:30:40 +00:00
wweir c347e15fe2 fix(stop): avoid unexpected stop 2025-05-14 13:17:18 +08:00
wweir 62176ffb65 feat: support fake site serve directory 2025-04-29 16:18:56 +08:00
wweir 5fe202314d feat: change log package to log/slog 2025-04-29 14:24:35 +08:00
wweir fc7183d70e feat: support force setting upstream dns server 2025-03-18 18:06:33 +08:00
wweir 4a9197a4ec fix: upstream all failed case 2024-11-03 00:40:23 +08:00
wweir 9eaf27fdcc fix(dns): serve interface parser 2024-10-12 10:26:29 +08:00
wweir ea6c2a29ad feat: reduce dependency on host DNS configuration 2024-10-12 10:10:52 +08:00
wweir 77c4154c7c fix: http_proxy 2024-10-12 07:20:39 +08:00
wweir eadb2f0bdf fix: sync example config with codes 2024-09-23 19:09:22 +08:00
wweir 19b61945ad fix: ipv6 support 2024-09-10 14:11:12 +08:00
wweir 3d6ee5e376 feat: support http(s)_proxy 2024-09-10 07:40:08 +08:00
wweir c0f704a5ad feat: add dns.serve_6 flag 2024-05-25 23:06:17 +08:00
wweir ac09663bc4 feat: multi ips, change default config 2024-05-14 14:06:46 +08:00
wweir 45c9eea6e6 feat: support listen on multi ip 2024-05-14 13:23:25 +08:00
wweir 0c83462b91 chore: update dependencies 2024-05-08 13:36:21 +08:00
wweir 15c02b55d8 feat: optimize for network changed case 2023-04-24 10:44:37 +08:00
wweir aca4636433 Merge pull request #27 from sower-proxy/dependabot/go_modules/golang.org/x/net-0.7.0
Bump golang.org/x/net from 0.0.0-20220531201128-c960675eff93 to 0.7.0
2023-03-08 19:06:59 +08:00
dependabot[bot] d0d635f8b0 Bump golang.org/x/net from 0.0.0-20220531201128-c960675eff93 to 0.7.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.0.0-20220531201128-c960675eff93 to 0.7.0.
- [Release notes](https://github.com/golang/net/releases)
- [Commits](https://github.com/golang/net/commits/v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-08 11:06:47 +00:00
wweir bde3b1bfa4 Merge pull request #26 from sower-proxy/dependabot/go_modules/golang.org/x/text-0.3.8
Bump golang.org/x/text from 0.3.7 to 0.3.8
2023-03-08 19:06:10 +08:00
dependabot[bot] 5dfefdf117 Bump golang.org/x/text from 0.3.7 to 0.3.8
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.3.7 to 0.3.8.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.3.7...v0.3.8)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-23 08:47:32 +00:00
wweir 3f63038631 fix: file based rules parser 2022-07-15 15:15:02 +08:00
wweir 8d18bd6c5b feat: support do not config block rules 2022-07-14 19:55:20 +08:00
52 changed files with 4448 additions and 629 deletions
+4 -1
View File
@@ -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
View File
@@ -15,6 +15,6 @@
<key>RunAtLoad</key>
<true/>
<key>WorkingDirectory</key>
<string>/usr/local/etc</string>
<string>/usr/local/etc/sower</string>
</dict>
</plist>
+1 -1
View File
@@ -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
+93
View File
@@ -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
View File
@@ -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"
+8 -8
View File
@@ -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
+1
View File
@@ -18,6 +18,7 @@
.vscode
.idea
**.env
/bin
**.mmdb
+132
View File
@@ -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
View File
@@ -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)
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+1 -1
View File
@@ -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
+26 -27
View File
@@ -1,12 +1,12 @@
# sower
[![GitHub release](http://img.shields.io/github/release/wweir/sower.svg?style=popout)](https://github.com/wweir/sower/releases)
[![Actions Status](https://github.com/wweir/sower/workflows/Go/badge.svg)](https://github.com/wweir/sower/actions)
[![GitHub issue](https://img.shields.io/github/issues/wweir/sower.svg?style=popout)](https://github.com/wweir/sower/issues)
[![GitHub star](https://img.shields.io/github/stars/wweir/sower.svg?style=popout)](https://github.com/wweir/sower/stargazers)
[![GitHub license](https://img.shields.io/github/license/wweir/sower.svg?style=popout)](LICENSE)
[![GitHub release](http://img.shields.io/github/release/sower-proxy/sower.svg?style=popout)](https://github.com/sower-proxy/sower/releases)
[![Actions Status](https://github.com/sower-proxy/sower/workflows/Go/badge.svg)](https://github.com/sower-proxy/sower/actions)
[![GitHub issue](https://img.shields.io/github/issues/sower-proxy/sower.svg?style=popout)](https://github.com/sower-proxy/sower/issues)
[![GitHub star](https://img.shields.io/github/stars/sower-proxy/sower.svg?style=popout)](https://github.com/sower-proxy/sower/stargazers)
[![GitHub license](https://img.shields.io/github/license/sower-proxy/sower.svg?style=popout)](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
![Architecture diagram](./sower.drawio.svg)
Detailed architecture and runtime design notes are in [ARCHITECTURE.md](./ARCHITECTURE.md).
+191 -119
View File
@@ -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:
}
}
+47
View File
@@ -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
View File
@@ -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
}
+124
View File
@@ -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
View File
@@ -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
}
+78
View File
@@ -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
View File
@@ -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
}
+57
View File
@@ -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
+56
View File
@@ -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
+79
View File
@@ -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")
}
}
+78
View File
@@ -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
}
+16
View File
@@ -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)
+16
View File
@@ -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)
+76
View File
@@ -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)
}
})
}
}
+24 -20
View File
@@ -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
)
+49 -75
View File
@@ -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=
+204
View File
@@ -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
}
+177
View File
@@ -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
View File
@@ -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},
+1 -1
View File
@@ -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() {
+13 -2
View File
@@ -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
}
}
+1 -1
View File
@@ -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() {
+59 -18
View File
@@ -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
}
}
+37 -1
View File
@@ -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) {
+107
View File
@@ -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
}
+21
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+783
View File
@@ -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() {}
+28
View File
@@ -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
View File
@@ -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
View File
@@ -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
+124
View File
@@ -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")
}
}
+10 -2
View File
@@ -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))
+119
View File
@@ -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)
}
}
+4 -9
View File
@@ -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
View File
@@ -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)
+112
View File
@@ -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")
}
}