mirror of
https://github.com/wweir/sower.git
synced 2026-10-19 16:14:33 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d514a52497 | ||
|
|
6f0b7d39b6 | ||
|
|
75156760db | ||
|
|
23110fc786 | ||
|
|
71eede9460 | ||
|
|
549ab2a990 | ||
|
|
8bbf5ca03d | ||
|
|
c3ef6ed88f | ||
|
|
0c8bdf6d9c | ||
|
|
53c55aef24 | ||
|
|
e705dd4dd3 | ||
|
|
6d410b104b | ||
|
|
6ccdcc131b | ||
|
|
2b072584e6 | ||
|
|
b2228f8643 | ||
|
|
5ab6acd7c7 | ||
|
|
a74b1204c0 | ||
|
|
677b0593b8 | ||
|
|
af61d38b9e | ||
|
|
f7a288af4c | ||
|
|
91b7f8e8c1 | ||
|
|
53b1de41d0 | ||
|
|
6c9b9d9597 | ||
|
|
1d8f0fdabe | ||
|
|
c347e15fe2 | ||
|
|
62176ffb65 | ||
|
|
5fe202314d | ||
|
|
fc7183d70e | ||
|
|
4a9197a4ec | ||
|
|
9eaf27fdcc | ||
|
|
ea6c2a29ad | ||
|
|
77c4154c7c | ||
|
|
eadb2f0bdf | ||
|
|
19b61945ad | ||
|
|
3d6ee5e376 | ||
|
|
c0f704a5ad | ||
|
|
ac09663bc4 | ||
|
|
45c9eea6e6 | ||
|
|
0c83462b91 | ||
|
|
15c02b55d8 | ||
|
|
aca4636433 | ||
|
|
d0d635f8b0 | ||
|
|
bde3b1bfa4 | ||
|
|
5dfefdf117 | ||
|
|
3f63038631 | ||
|
|
8d18bd6c5b | ||
|
|
98a5f5702c | ||
|
|
2948ffb7db | ||
|
|
b85c3a63d7 | ||
|
|
47e8076c06 | ||
|
|
10dc20e9e8 | ||
|
|
19dcf8e0d7 | ||
|
|
795bf73fb2 | ||
|
|
3412643b6e | ||
|
|
6fc97a3c9f | ||
|
|
78d36b0c19 | ||
|
|
5ebd33b0d7 | ||
|
|
b1f152cc0c | ||
|
|
c0b77da05a | ||
|
|
bad7ae702a | ||
|
|
05a1a3249a | ||
|
|
0f029f8c15 | ||
|
|
8c9a0bc1dd | ||
|
|
465ff316fd | ||
|
|
a86de61a45 | ||
|
|
ffca4220d6 | ||
|
|
48e9680c03 | ||
|
|
dabcf998c9 | ||
|
|
133684e5d2 | ||
|
|
a28a211d2d | ||
|
|
28696acafb | ||
|
|
38c7391ceb | ||
|
|
65234b1666 | ||
|
|
adca6606d2 | ||
|
|
cfc1afb710 | ||
|
|
397825556c | ||
|
|
8694ff6edd | ||
|
|
a38affd97b | ||
|
|
74202aad83 | ||
|
|
59dfa934a2 | ||
|
|
8bfb26ba3f | ||
|
|
fbc6c93395 | ||
|
|
2d269468ea | ||
|
|
d7c0ba112e | ||
|
|
d31ba7ef79 | ||
|
|
8d32bca742 | ||
|
|
426799a961 | ||
|
|
5cf8df2a7a | ||
|
|
7d024f3bfe | ||
|
|
585cc01744 | ||
|
|
8307128844 | ||
|
|
dab1a78bbf | ||
|
|
13a6be47a4 | ||
|
|
56b8e35fe7 |
@@ -1,17 +0,0 @@
|
||||
# Compile
|
||||
FROM golang:1.14-alpine AS compiler
|
||||
|
||||
RUN apk add --no-cache git make
|
||||
|
||||
# enable go modules
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
|
||||
RUN CGO_ENABLED=0 make build
|
||||
|
||||
|
||||
# Build image
|
||||
FROM scratch
|
||||
|
||||
COPY --from=compiler /src/sower /sower
|
||||
ENTRYPOINT [ "/sower" ]
|
||||
@@ -0,0 +1,100 @@
|
||||
# 远程、上游连接方式
|
||||
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",
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>sower</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/local/bin/sower</string>
|
||||
<string>-f</string>
|
||||
<string>/usr/local/etc/sower/sower.hcl</string>
|
||||
</array>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/usr/local/etc/sower</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=sower client service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/etc/sower
|
||||
ExecStart=/usr/local/bin/sower -c /etc/sower/sower.hcl
|
||||
Restart=on-failure
|
||||
RestartSec=1min
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -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",
|
||||
]
|
||||
@@ -0,0 +1,89 @@
|
||||
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: []
|
||||
|
||||
# 直接访问规则。被命中的请求,将被直接访问,不再通过代理
|
||||
direct:
|
||||
file: "https://raw.githubusercontent.com/pexcn/daily/gh-pages/chinalist/chinalist.txt"
|
||||
rules:
|
||||
- "imap.*.*"
|
||||
- "imap.*.*.*"
|
||||
- "smtp.*.*"
|
||||
- "smtp.*.*.*"
|
||||
- "pop.*.*"
|
||||
- "pop.*.*.*"
|
||||
- "pop3.*.*"
|
||||
- "pop3.*.*.*"
|
||||
- "**.cn"
|
||||
|
||||
# 代理访问规则。被命中的请求,将通过代理进行转发
|
||||
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 模式下生效
|
||||
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"
|
||||
@@ -0,0 +1 @@
|
||||
CreateObject("WScript.Shell").Run "C:\Software\Sower\sower.exe -f sower.hcl",0
|
||||
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=sowerd server service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/sowerd
|
||||
Environment="FAKE_SITE=127.0.0.1:8080"
|
||||
Environment="PASSWORD=I_am_Passw0rd"
|
||||
Restart=on-failure
|
||||
RestartSec=1min
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -8,7 +8,9 @@ jobs:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-go@v2-beta
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.17
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: test and build
|
||||
|
||||
@@ -8,46 +8,45 @@ jobs:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-go@v2-beta
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.18
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: build matrix
|
||||
run: |
|
||||
cp conf/sower.toml sower.toml
|
||||
mv .github/* . || true
|
||||
|
||||
GOOS=linux GOARCH=amd64 make build
|
||||
tar czvf sower-linux-amd64.tar.gz sower sower.toml
|
||||
GOOS=linux GOARCH=386 make build
|
||||
tar czvf sower-linux-386.tar.gz sower sower.toml
|
||||
GOOS=linux GOARCH=arm make build
|
||||
tar czvf sower-linux-arm.tar.gz sower sower.toml
|
||||
GOOS=linux GOARCH=arm64 make build
|
||||
tar czvf sower-linux-arm64.tar.gz sower sower.toml
|
||||
GOOS=linux GOARCH=mips make build
|
||||
tar czvf sower-linux-mips.tar.gz sower sower.toml
|
||||
GOOS=linux GOARCH=mipsle make build
|
||||
tar czvf sower-linux-mipsle.tar.gz sower sower.toml
|
||||
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.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.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.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.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.toml sower.service sowerd.service
|
||||
make clean
|
||||
|
||||
GOOS=darwin GOARCH=amd64 make build
|
||||
tar czvf sower-darwin-amd64.tar.gz sower sower.toml
|
||||
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.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.toml sower.plist
|
||||
make clean
|
||||
|
||||
GOOS=windows GOARCH=amd64 make build
|
||||
tar czvf sower-windows-amd64.tar.gz sower.exe sower.toml
|
||||
GOOS=windows GOARCH=386 make build
|
||||
tar czvf sower-windows-386.tar.gz sower.exe sower.toml
|
||||
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.toml sower_run.vbs
|
||||
make clean
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@master
|
||||
- uses: ncipollo/release-action@v1
|
||||
with:
|
||||
files: |
|
||||
sower-linux-amd64.tar.gz
|
||||
sower-linux-386.tar.gz
|
||||
sower-linux-arm.tar.gz
|
||||
sower-linux-arm64.tar.gz
|
||||
sower-linux-mips.tar.gz
|
||||
sower-linux-mipsle.tar.gz
|
||||
sower-darwin-amd64.tar.gz
|
||||
sower-windows-amd64.tar.gz
|
||||
sower-windows-386.tar.gz
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
artifacts: "*.tar.gz"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
+14
-3
@@ -17,6 +17,17 @@
|
||||
*.swo
|
||||
.vscode
|
||||
.idea
|
||||
sower
|
||||
sower.exe*
|
||||
/sower.toml
|
||||
**.env
|
||||
/bin
|
||||
|
||||
|
||||
**.mmdb
|
||||
/cmd/client/client
|
||||
/cmd/server/server
|
||||
/sower
|
||||
/sowerd
|
||||
/*.yml
|
||||
/*.yaml
|
||||
/*.toml
|
||||
/*.hcl
|
||||
/*.tf
|
||||
@@ -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})))
|
||||
```
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
# 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 traffic, parse the target host from the request line. For HTTPS transparent traffic, peek the TLS ClientHello to extract SNI without terminating TLS locally. For SOCKS5 traffic, read the SOCKS5 target address. 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.
|
||||
- `sowerd` prefers the user cache directory for ACME state, but falls back to `/var/cache/sower` so systemd services can start without `HOME`/`XDG_CACHE_HOME` or a config file.
|
||||
|
||||
## 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)
|
||||
@@ -1,12 +1,25 @@
|
||||
MAKEFLAGS += --jobs all
|
||||
GO:=CGO_ENABLED=0 go
|
||||
|
||||
default: test build
|
||||
|
||||
test:
|
||||
go vet ./...
|
||||
go list ./... | grep -v internal | xargs go test
|
||||
build:
|
||||
go build -ldflags "-w -s \
|
||||
-X conf.version=$(shell git describe --tags --always) \
|
||||
-X conf.date=$(shell date +%Y-%m-%d)"
|
||||
image:
|
||||
docker build -t sower -f .github/Dockerfile .
|
||||
${GO} vet ./...
|
||||
${GO} test ./...
|
||||
|
||||
build: sower sowerd
|
||||
.PHONY: sower
|
||||
sower:
|
||||
${GO} build -ldflags "\
|
||||
-X main.version=$(shell git describe --tags --always) \
|
||||
-X main.date=$(shell date +%Y-%m-%d)" \
|
||||
-o sower ./cmd/sower
|
||||
.PHONY: sowerd
|
||||
sowerd:
|
||||
${GO} build -ldflags "\
|
||||
-X main.version=$(shell git describe --tags --always) \
|
||||
-X main.date=$(shell date +%Y-%m-%d)" \
|
||||
-o sowerd ./cmd/sowerd
|
||||
|
||||
clean:
|
||||
rm -f sower sowerd sower.exe sowerd.exe
|
||||
|
||||
@@ -1,90 +1,73 @@
|
||||
# sower
|
||||
[](https://github.com/wweir/sower/releases)
|
||||
[](https://github.com/wweir/sower/actions)
|
||||
[](https://hub.docker.com/r/wweir/sower)
|
||||
[](https://github.com/wweir/sower/issues)
|
||||
[](https://github.com/wweir/sower/stargazers)
|
||||
[](LICENSE)
|
||||
|
||||
[](https://github.com/sower-proxy/sower/releases)
|
||||
[](https://github.com/sower-proxy/sower/actions)
|
||||
[](https://github.com/sower-proxy/sower/issues)
|
||||
[](https://github.com/sower-proxy/sower/stargazers)
|
||||
[](LICENSE)
|
||||
|
||||
中文介绍见 [Wiki](https://github.com/wweir/sower/wiki)
|
||||
中文介绍见 [Wiki](https://github.com/sower-proxy/sower/wiki)
|
||||
|
||||
The sower is a cross-platform intelligent transparent proxy tool.
|
||||
|
||||
The first time you visit a new website, the sower will detect if the domain is accessible and add it in the dynamic detect list. So, you do not need to care about the rules, sower will handle it intelligently.
|
||||
|
||||
Sower provider both http_proxy/https_proxy and DNS-based proxy. All these kinds of proxy support intelligent router. You can also port-forward any TCP request to remote, such as SSH / SMTP / POP3.
|
||||
|
||||
You can enjoy it by setting http_proxy or your DNS without any other settings.
|
||||
|
||||
If you already have another proxy solution, you can use it's socks5(h) service as a parent proxy to enjoy the sower's intelligent router.
|
||||
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.
|
||||
|
||||
If you already have another proxy solution, you can use its socks5(h) service as a parent proxy to enjoy sower's intelligent routing.
|
||||
|
||||
## Installation
|
||||
To enjoy the sower, you need to deploy sower on both server-side and client-side.
|
||||
|
||||
The installation script has been integrated into the sower. You can install sower as system service by running `./sower -install 'xxx'` in terminal on the three most popular operating system platforms: Linux / Windows and masOS.
|
||||
To enjoy sower, you need to deploy sower on both server-side(sowerd) and client-side(sower).
|
||||
|
||||
## Server
|
||||
*If you already have another proxy solution with socks5h support, you can skip server-side.*
|
||||
## Sowerd
|
||||
|
||||
At the server-side, the sower runs just like a web server proxy.
|
||||
It redirects HTTP requests to HTTPS and proxy https requests to the upstream HTTP service.
|
||||
You can use your certificate or use the auto-generated certificate by the sower.
|
||||
_If you want to use sower as a secondary proxy to provide intelligent routing, you can skip sowerd._
|
||||
|
||||
What you must set is the upstream HTTP service. You can set it by parameter `-s`, eg:
|
||||
``` shell
|
||||
# sower -s 127.0.0.1:8080
|
||||
```
|
||||
On the server-side, sowerd runs just like a web server proxy. It will bind two ports `80` / `443`.
|
||||
|
||||
## Client
|
||||
The easiest way to run it is:
|
||||
``` shell
|
||||
# sower -c aa.bb.cc # the `aa.bb.cc` can also be `socks5h://127.0.0.1:1080`
|
||||
```
|
||||
But a configuration file is recommended to persist dynamic rules in client side.
|
||||
`sowerd` validates its configuration at startup, redacts secrets from logs, and exits immediately if `80` / `443` listeners cannot be established.
|
||||
|
||||
There are 3 kinds of proxy solutions, they are HTTP(S)_PROXY / DNS-based proxy / port-forward.
|
||||
When `sowerd` runs as a system service without `HOME` or `XDG_CACHE_HOME`, it falls back to `/var/cache/sower` for certificate cache state so environment-only startup still works without a config file.
|
||||
|
||||
### HTTP(S)_PROXY
|
||||
An HTTP(S)_PROXY listening on `:8080` is set by default if you run sower as client mode.
|
||||
You can use your own certificate or the certificate automatically applied for by sowerd from [`Let's Encrypt`](https://letsencrypt.org/).
|
||||
|
||||
### DNS-based proxy
|
||||
**DNS-based** solution is not recommanded now, for incompatible with TLS 1.3 esni extention. DNS flush logic has been dropped, you should flush your dns cache manually
|
||||
in Windows and macOS.
|
||||
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.
|
||||
|
||||
You can set the `dns_serve_ip` field in the configuration file to start the DNS-based proxy. You should also set the value of `dns_upstream` as your default DNS in OS.
|
||||
There are two ways to run the sowerd service:
|
||||
|
||||
If you want to enjoy the full experience provided by the sower, you can take sower as your private DNS on a long-running server and set it as your default DNS in your router.
|
||||
1. Run the command with root permission
|
||||
|
||||
### port-forward
|
||||
The port-forward can be only setted in configuration file, you can set it in section `client.port_mapping`, eg:
|
||||
``` toml
|
||||
[client.port_mapping]
|
||||
":2222"="aa.bb.cc:22"
|
||||
```
|
||||
```shell
|
||||
# sowerd -password XXX -fake_site 127.0.0.1:8080
|
||||
```
|
||||
|
||||
2. Install as a systemd service interactively
|
||||
|
||||
```shell
|
||||
sudo sowerd -i
|
||||
```
|
||||
|
||||
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/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
|
||||
```
|
||||
|
||||
2. Change your DNS server to `127.0.0.1`.
|
||||
|
||||
3. Set your proxy to `socks5h://127.0.0.1:1080`.
|
||||
|
||||
## Architecture
|
||||
```
|
||||
relay <--+ +-> target
|
||||
http service | | service
|
||||
+-------+-------+----+
|
||||
| sower server |
|
||||
+----^-------^-------+
|
||||
80 443
|
||||
301 http -+ +----- https
|
||||
to https | service
|
||||
protected
|
||||
by tls
|
||||
socks5 |
|
||||
dns <---+ ^ | +--> direct
|
||||
relay | | | | request
|
||||
+---+---+----+--+----+
|
||||
| sower client |
|
||||
+----^--^----^---^---+
|
||||
| | | |
|
||||
dns --+ + + +-- port
|
||||
80 http(s) forward
|
||||
443 proxy
|
||||
```
|
||||
|
||||

|
||||
|
||||
Detailed architecture and runtime design notes are in [ARCHITECTURE.md](./ARCHITECTURE.md).
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"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/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 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: 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(),
|
||||
".toml": aconfigtoml.New(),
|
||||
".hcl": aconfighcl.New(),
|
||||
},
|
||||
}).Load(); err != nil {
|
||||
slog.Error("load config", "error", err, "config", conf)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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() {
|
||||
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...)
|
||||
|
||||
errCh := make(chan error, 8)
|
||||
go watchServeErrors(ctx, stop, errCh)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
if err := startTCPService(ctx, errCh, "http proxy", net.JoinHostPort(ip, "80"), func(ctx context.Context, ln net.Listener) error {
|
||||
return ServeHTTP(ctx, ln, r)
|
||||
}); err != nil {
|
||||
slog.Error("start http proxy", "error", err, "listen_on", net.JoinHostPort(ip, "80"))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := startTCPService(ctx, errCh, "https proxy", net.JoinHostPort(ip, "443"), func(ctx context.Context, ln net.Listener) error {
|
||||
return ServeHTTPS(ctx, ln, r)
|
||||
}); err != nil {
|
||||
slog.Error("start https proxy", "error", err, "listen_on", net.JoinHostPort(ip, "443"))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := startDNSService(ctx, errCh, net.JoinHostPort(ip, "53"), r); err != nil {
|
||||
slog.Error("start dns proxy", "error", err, "listen_on", net.JoinHostPort(ip, "53"))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if conf.Socks5.Disable {
|
||||
slog.Info("SOCKS5 proxy disabled")
|
||||
} else if err := startTCPService(ctx, errCh, "socks5 proxy", conf.Socks5.Addr, func(ctx context.Context, ln net.Listener) error {
|
||||
return ServeSocks5(ctx, ln, r)
|
||||
}); err != nil {
|
||||
slog.Error("start socks5 proxy", "error", err, "listen_on", conf.Socks5.Addr)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
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)
|
||||
}
|
||||
|
||||
slog.Info("loaded rules, proxy started", "took", time.Since(start),
|
||||
"blockRule", r.BlockRule.Count, "directRule", r.DirectRule.Count, "proxyRule", r.ProxyRule.Count)
|
||||
runtime.GC()
|
||||
|
||||
<-ctx.Done()
|
||||
slog.Info("shutting down sower", "reason", ctx.Err())
|
||||
}
|
||||
|
||||
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 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 := 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{
|
||||
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))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
loadFn = func() (io.ReadCloser, error) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
resp.Body.Close()
|
||||
return nil, errors.Errorf("status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return resp.Body, nil
|
||||
}
|
||||
}
|
||||
|
||||
// load rule file, retry 10 times
|
||||
rc, err := loadFn()
|
||||
for i := time.Duration(1); i < 10; i++ {
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
slog.Error("fetch rule file", "error", ctx.Err(), "file", file)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// wait: 28.5s
|
||||
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()
|
||||
}
|
||||
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)
|
||||
if gr, err := gzip.NewReader(rc); err == nil {
|
||||
rc = gr
|
||||
defer gr.Close()
|
||||
}
|
||||
|
||||
br := bufio.NewReader(rc)
|
||||
for {
|
||||
line, _, err := br.ReadLine()
|
||||
if err == io.EOF {
|
||||
return
|
||||
} else if err != nil {
|
||||
slog.Error("read line", "error", err, "file", file)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.TrimSpace(string(line)) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
ch <- string(line)
|
||||
}
|
||||
}()
|
||||
|
||||
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:
|
||||
}
|
||||
}
|
||||
|
||||
func startTCPService(ctx context.Context, errCh chan<- error, service, addr string, serve func(context.Context, net.Listener) error) error {
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listen on %s: %w", addr, err)
|
||||
}
|
||||
|
||||
slog.Info("service listening", "service", service, "network", "tcp", "addr", addr)
|
||||
go closeOnDone(ctx, ln)
|
||||
go func() {
|
||||
reportServeError(errCh, service, serve(ctx, ln))
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func startDNSService(ctx context.Context, errCh chan<- error, addr string, handler dns.Handler) error {
|
||||
pc, err := net.ListenPacket("udp", addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listen on %s: %w", addr, err)
|
||||
}
|
||||
|
||||
server := &dns.Server{
|
||||
PacketConn: pc,
|
||||
Handler: handler,
|
||||
}
|
||||
slog.Info("service listening", "service", "dns proxy", "network", "udp", "addr", addr)
|
||||
go shutdownDNSServerOnDone(ctx, server)
|
||||
go func() {
|
||||
err := server.ActivateAndServe()
|
||||
if err != nil && !stderrors.Is(err, net.ErrClosed) {
|
||||
reportServeError(errCh, "dns proxy", fmt.Errorf("serve on %s: %w", addr, err))
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func watchServeErrors(ctx context.Context, stop context.CancelFunc, errCh <-chan error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case err := <-errCh:
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
slog.Error("serve failed", "error", err)
|
||||
stop()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
utls "github.com/refraction-networking/utls"
|
||||
"github.com/sower-proxy/conns/relay"
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
proxyDialTimeout = 10 * time.Second
|
||||
proxyReadTimeout = 15 * time.Second
|
||||
|
||||
tlsRecordHeaderLen = 5
|
||||
tlsRecordTypeHandshake = 22
|
||||
tlsHandshakeTypeHello = 1
|
||||
maxTLSPlaintextRecordLen = 64 * 1024
|
||||
)
|
||||
|
||||
func GenProxyDial(proxyType, proxyHost, proxyPassword, dns string, tlsOptions upstreamtls.Options) (router.ProxyDialFn, error) {
|
||||
var proxy transport.Transport
|
||||
var dialFn func() (net.Conn, error)
|
||||
|
||||
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
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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 dialer.Dial("tcp", proxyHost)
|
||||
}
|
||||
default:
|
||||
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, fmt.Errorf("invalid addr(%s:%d)", host, port)
|
||||
}
|
||||
|
||||
conn, err := dialFn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := proxy.Wrap(conn, host, port); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
rereadConn := reread.New(conn)
|
||||
defer rereadConn.Close()
|
||||
|
||||
_ = rereadConn.SetDeadline(time.Now().Add(proxyReadTimeout))
|
||||
req, err := http.ReadRequest(bufio.NewReader(rereadConn))
|
||||
if err != nil {
|
||||
slog.Error("read http request", "error", err)
|
||||
return
|
||||
}
|
||||
_ = rereadConn.SetDeadline(time.Time{})
|
||||
|
||||
rc, err := r.ProxyDial("tcp", req.Host, 80)
|
||||
if err != nil {
|
||||
slog.Error("dial proxy", "error", err, "host", req.Host, "req", req.URL)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
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 handleHTTPSConn(conn net.Conn, r *router.Router) {
|
||||
start := time.Now()
|
||||
rereadConn := reread.New(conn)
|
||||
defer rereadConn.Close()
|
||||
|
||||
_ = rereadConn.SetDeadline(time.Now().Add(proxyReadTimeout))
|
||||
domain, err := peekTLSClientHelloServerName(rereadConn)
|
||||
if err != nil {
|
||||
slog.Debug("read tls client hello", "error", err)
|
||||
return
|
||||
}
|
||||
if domain == "" {
|
||||
slog.Debug("tls client hello missing server name")
|
||||
return
|
||||
}
|
||||
_ = rereadConn.SetDeadline(time.Time{})
|
||||
|
||||
rc, err := r.ProxyDial("tcp", domain, 443)
|
||||
if err != nil {
|
||||
slog.Error("dial proxy", "error", err, "host", domain)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
rereadConn.Stop().Reread()
|
||||
err = relay.Relay(rereadConn, rc)
|
||||
if err != nil {
|
||||
slog.Debug("serve https", "error", err, "host", domain, "spend", time.Since(start))
|
||||
}
|
||||
}
|
||||
|
||||
func peekTLSClientHelloServerName(conn io.Reader) (string, error) {
|
||||
clientHello, err := readTLSClientHello(conn)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hello := utls.UnmarshalClientHello(clientHello)
|
||||
if hello == nil {
|
||||
return "", fmt.Errorf("parse tls client hello")
|
||||
}
|
||||
return hello.ServerName, nil
|
||||
}
|
||||
|
||||
func readTLSClientHello(conn io.Reader) ([]byte, error) {
|
||||
clientHello := make([]byte, 0, 1024)
|
||||
|
||||
for {
|
||||
header := make([]byte, tlsRecordHeaderLen)
|
||||
if _, err := io.ReadFull(conn, header); err != nil {
|
||||
return nil, fmt.Errorf("read tls record header: %w", err)
|
||||
}
|
||||
if header[0] != tlsRecordTypeHandshake {
|
||||
return nil, fmt.Errorf("unexpected tls record type %d", header[0])
|
||||
}
|
||||
|
||||
recordLen := int(header[3])<<8 | int(header[4])
|
||||
if recordLen <= 0 || recordLen > maxTLSPlaintextRecordLen {
|
||||
return nil, fmt.Errorf("invalid tls record length %d", recordLen)
|
||||
}
|
||||
|
||||
recordBody := make([]byte, recordLen)
|
||||
if _, err := io.ReadFull(conn, recordBody); err != nil {
|
||||
return nil, fmt.Errorf("read tls record body: %w", err)
|
||||
}
|
||||
clientHello = append(clientHello, recordBody...)
|
||||
|
||||
if len(clientHello) < 4 {
|
||||
continue
|
||||
}
|
||||
if clientHello[0] != tlsHandshakeTypeHello {
|
||||
return nil, fmt.Errorf("unexpected tls handshake type %d", clientHello[0])
|
||||
}
|
||||
|
||||
helloLen := int(clientHello[1])<<16 | int(clientHello[2])<<8 | int(clientHello[3])
|
||||
if helloLen == 0 {
|
||||
return nil, fmt.Errorf("empty tls client hello")
|
||||
}
|
||||
if len(clientHello) < 4+helloLen {
|
||||
continue
|
||||
}
|
||||
|
||||
return clientHello[:4+helloLen], nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleSocks5Conn(conn net.Conn, r *router.Router) {
|
||||
defer conn.Close()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
req, err := http.ReadRequest(bufio.NewReader(rereadConn))
|
||||
if err != nil {
|
||||
slog.Error("read http request", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
host, port, err := router.ParseHostPort(req.Host, req.URL)
|
||||
if err != nil {
|
||||
rereadConn.Stop().Write([]byte("HTTP/1.1 400 Bad Request\r\n\r\n"))
|
||||
return
|
||||
}
|
||||
|
||||
rc, err := r.Dial("tcp", host, port)
|
||||
if err != nil {
|
||||
writeHTTPProxyError(rereadConn.Stop(), err)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
if req.Method == http.MethodConnect {
|
||||
rereadConn.Stop().Reset()
|
||||
if _, err := rereadConn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n")); err != nil {
|
||||
slog.Debug("write connect response", "error", err, "host", host, "port", port)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
rereadConn.Stop().Reread()
|
||||
}
|
||||
|
||||
if err := relay.Relay(rereadConn, rc); err != nil {
|
||||
slog.Debug("serve proxy request", "error", err, "host", host, "port", port, "method", req.Method)
|
||||
}
|
||||
}
|
||||
|
||||
func shouldRetryAccept(ctx context.Context, protocol string, err error) bool {
|
||||
if err == nil || ctx.Err() != nil {
|
||||
return false
|
||||
}
|
||||
if ne, ok := err.(net.Error); ok && ne.Temporary() {
|
||||
slog.Warn("temporary accept failed", "protocol", protocol, "error", err)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func wrapAcceptErr(ctx context.Context, protocol string, err error) error {
|
||||
if err == nil || ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("accept %s connection: %w", protocol, err)
|
||||
}
|
||||
|
||||
func writeHTTPProxyError(conn net.Conn, err error) {
|
||||
status := "502 Bad Gateway"
|
||||
if stderrors.Is(err, router.ErrBlocked) {
|
||||
status = "403 Forbidden"
|
||||
}
|
||||
if _, writeErr := conn.Write([]byte("HTTP/1.1 " + status + "\r\n\r\n")); writeErr != nil {
|
||||
slog.Debug("write http proxy error", "error", writeErr, "status", status)
|
||||
}
|
||||
}
|
||||
|
||||
func routeSocks5ReplyCode(err error) byte {
|
||||
if stderrors.Is(err, router.ErrBlocked) {
|
||||
return socks5.RepConnectionNotAllowed
|
||||
}
|
||||
return socks5.RepGeneralFailure
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"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 TestPeekTLSClientHelloServerName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
server, client := net.Pipe()
|
||||
defer client.Close()
|
||||
|
||||
hello := generateTLSClientHelloRecord(t, "github.com")
|
||||
go func() {
|
||||
_, _ = client.Write(hello)
|
||||
_ = client.Close()
|
||||
}()
|
||||
|
||||
got, err := peekTLSClientHelloServerName(server)
|
||||
_ = server.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("peek tls client hello: %v", err)
|
||||
}
|
||||
if got != "github.com" {
|
||||
t.Fatalf("unexpected server name: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleHTTPSConnRelaysClientHelloToUpstream(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
downstreamServer, downstreamClient := net.Pipe()
|
||||
defer downstreamClient.Close()
|
||||
|
||||
upstreamClient, upstreamServer := net.Pipe()
|
||||
defer upstreamServer.Close()
|
||||
|
||||
hello := generateTLSClientHelloRecord(t, "github.com")
|
||||
response := []byte("upstream-response")
|
||||
|
||||
var (
|
||||
gotHost string
|
||||
gotPort uint16
|
||||
)
|
||||
r := &router.Router{
|
||||
BlockRule: suffixtree.NewNodeFromRules(),
|
||||
DirectRule: suffixtree.NewNodeFromRules(),
|
||||
ProxyRule: suffixtree.NewNodeFromRules(),
|
||||
ProxyDial: func(network, host string, port uint16) (net.Conn, error) {
|
||||
gotHost = host
|
||||
gotPort = port
|
||||
return upstreamClient, nil
|
||||
},
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
handleHTTPSConn(downstreamServer, r)
|
||||
}()
|
||||
|
||||
writeErrCh := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := downstreamClient.Write(hello)
|
||||
writeErrCh <- err
|
||||
}()
|
||||
|
||||
gotHello := make([]byte, len(hello))
|
||||
upstreamServer.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||||
if _, err := io.ReadFull(upstreamServer, gotHello); err != nil {
|
||||
t.Fatalf("read upstream client hello: %v", err)
|
||||
}
|
||||
if string(gotHello) != string(hello) {
|
||||
t.Fatal("client hello was not relayed intact")
|
||||
}
|
||||
if err := <-writeErrCh; err != nil {
|
||||
t.Fatalf("write downstream client hello: %v", err)
|
||||
}
|
||||
|
||||
readErrCh := make(chan error, 1)
|
||||
gotResp := make([]byte, len(response))
|
||||
go func() {
|
||||
_, err := io.ReadFull(downstreamClient, gotResp)
|
||||
readErrCh <- err
|
||||
}()
|
||||
|
||||
upstreamServer.SetWriteDeadline(time.Now().Add(2 * time.Second))
|
||||
if _, err := upstreamServer.Write(response); err != nil {
|
||||
t.Fatalf("write upstream response: %v", err)
|
||||
}
|
||||
if err := <-readErrCh; err != nil {
|
||||
t.Fatalf("read downstream response: %v", err)
|
||||
}
|
||||
if string(gotResp) != string(response) {
|
||||
t.Fatalf("unexpected downstream response: %q", gotResp)
|
||||
}
|
||||
if gotHost != "github.com" || gotPort != 443 {
|
||||
t.Fatalf("unexpected proxy target: %s:%d", gotHost, gotPort)
|
||||
}
|
||||
|
||||
_ = downstreamClient.Close()
|
||||
_ = upstreamServer.Close()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func generateTLSClientHelloRecord(t *testing.T, serverName string) []byte {
|
||||
t.Helper()
|
||||
|
||||
clientConn, serverConn := net.Pipe()
|
||||
defer clientConn.Close()
|
||||
defer serverConn.Close()
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
tlsConn := tls.Client(clientConn, &tls.Config{
|
||||
ServerName: serverName,
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
errCh <- tlsConn.Handshake()
|
||||
_ = tlsConn.Close()
|
||||
}()
|
||||
|
||||
serverConn.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||||
header := make([]byte, tlsRecordHeaderLen)
|
||||
if _, err := io.ReadFull(serverConn, header); err != nil {
|
||||
t.Fatalf("read tls record header: %v", err)
|
||||
}
|
||||
|
||||
recordLen := int(header[3])<<8 | int(header[4])
|
||||
record := make([]byte, len(header)+recordLen)
|
||||
copy(record, header)
|
||||
if _, err := io.ReadFull(serverConn, record[len(header):]); err != nil {
|
||||
t.Fatalf("read tls record body: %v", err)
|
||||
}
|
||||
|
||||
_ = serverConn.Close()
|
||||
<-errCh
|
||||
return record
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
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/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
|
||||
systemCacheDir = "/var/cache/sower"
|
||||
)
|
||||
|
||||
var (
|
||||
version, date string
|
||||
)
|
||||
|
||||
func init() {
|
||||
setLogger(slog.LevelInfo)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if hasInstallFlag(os.Args[1:]) {
|
||||
if err := install.InstallService(stdinConfirm); err != nil {
|
||||
slog.Error("install service", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
dir, fallbackErr := resolveCacheDir(os.UserCacheDir, systemCacheDir)
|
||||
if fallbackErr != nil {
|
||||
slog.Warn("user cache dir unavailable, fallback to system cache dir",
|
||||
"error", fallbackErr,
|
||||
"dir", dir)
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
return "", fmt.Errorf("create cache dir %s: %w", dir, err)
|
||||
}
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
func resolveCacheDir(userCacheDir func() (string, error), fallbackDir string) (string, error) {
|
||||
base, err := userCacheDir()
|
||||
if err != nil {
|
||||
return fallbackDir, err
|
||||
}
|
||||
return filepath.Join(base, "sower"), nil
|
||||
}
|
||||
|
||||
func buildTLSConfig(cacheDir string, cfg config.SowerdConfig) (*autocert.Manager, *tls.Config, error) {
|
||||
certManager := &autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
Email: cfg.Cert.Email,
|
||||
Cache: autocert.DirCache(cacheDir),
|
||||
}
|
||||
|
||||
tlsConf := &tls.Config{
|
||||
GetCertificate: certManager.GetCertificate,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
NextProtos: []string{"http/1.1", "h2"},
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
target := "https://" + r.Host + r.URL.RequestURI()
|
||||
http.Redirect(w, r, target, http.StatusPermanentRedirect)
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 handleConn(conn net.Conn, fakeSite string, sower *transportSower.Sower, trojan *trojan.Trojan) {
|
||||
rereadConn := reread.New(conn)
|
||||
defer rereadConn.Close()
|
||||
|
||||
_ = rereadConn.SetReadDeadline(time.Now().Add(probeTimeout))
|
||||
|
||||
var (
|
||||
addr net.Addr
|
||||
dur time.Duration
|
||||
err error
|
||||
)
|
||||
defer func() {
|
||||
deferlog.DebugWarn(err, "relay conn", "took", dur, "addr", addr)
|
||||
}()
|
||||
|
||||
rereadConn.Reread()
|
||||
if addr, err = sower.Unwrap(rereadConn); err == nil {
|
||||
rereadConn.Stop()
|
||||
_ = rereadConn.SetReadDeadline(time.Time{})
|
||||
dur, err = relay.RelayTo(rereadConn, addr.String())
|
||||
return
|
||||
}
|
||||
|
||||
rereadConn.Reread()
|
||||
if addr, err = trojan.Unwrap(rereadConn); err == nil {
|
||||
rereadConn.Stop()
|
||||
_ = rereadConn.SetReadDeadline(time.Time{})
|
||||
dur, err = relay.RelayTo(rereadConn, addr.String())
|
||||
return
|
||||
}
|
||||
|
||||
rereadConn.Stop().Reread()
|
||||
_ = rereadConn.SetReadDeadline(time.Time{})
|
||||
dur, err = relay.RelayTo(rereadConn, fakeSite)
|
||||
}
|
||||
|
||||
func sanitizeConfig(cfg config.SowerdConfig) map[string]any {
|
||||
return map[string]any{
|
||||
"log_level": cfg.LogLevel.String(),
|
||||
"serve_ip": cfg.ServeIP,
|
||||
"fake_site": cfg.FakeSite,
|
||||
"cert": map[string]any{
|
||||
"email": cfg.Cert.Email,
|
||||
"cert_config": cfg.Cert.Cert != "",
|
||||
"key_config": cfg.Cert.Key != "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func hasInstallFlag(args []string) bool {
|
||||
for _, arg := range args {
|
||||
switch arg {
|
||||
case "-i", "--install":
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func stdinConfirm(label string) bool {
|
||||
fmt.Printf("%s [y/N]: ", label)
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
if !scanner.Scan() {
|
||||
return false
|
||||
}
|
||||
answer := strings.TrimSpace(strings.ToLower(scanner.Text()))
|
||||
return answer == "y" || answer == "yes"
|
||||
}
|
||||
|
||||
func isLoopbackRemoteAddr(remoteAddr string) bool {
|
||||
host, _, err := net.SplitHostPort(remoteAddr)
|
||||
if err != nil {
|
||||
host = remoteAddr
|
||||
}
|
||||
host = strings.Trim(host, "[]")
|
||||
|
||||
ip := net.ParseIP(host)
|
||||
return ip != nil && ip.IsLoopback()
|
||||
}
|
||||
|
||||
func setLogger(level slog.Level) {
|
||||
fi, err := os.Stdout.Stat()
|
||||
noColor := err != nil || (fi.Mode()&os.ModeCharDevice) == 0
|
||||
deferlog.SetDefault(slog.New(tint.NewHandler(os.Stdout, &tint.Options{
|
||||
AddSource: true,
|
||||
NoColor: noColor,
|
||||
Level: level,
|
||||
})))
|
||||
}
|
||||
|
||||
func shutdownHTTP(ctx context.Context, server *http.Server) error {
|
||||
if err := server.Shutdown(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return fmt.Errorf("shutdown http server: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCacheDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
userCache func() (string, error)
|
||||
fallbackDir string
|
||||
wantDir string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "user cache dir available",
|
||||
userCache: func() (string, error) {
|
||||
return "/tmp/cache", nil
|
||||
},
|
||||
fallbackDir: "/var/cache/sower",
|
||||
wantDir: filepath.Join("/tmp/cache", "sower"),
|
||||
},
|
||||
{
|
||||
name: "fallback to system cache dir",
|
||||
userCache: func() (string, error) {
|
||||
return "", errors.New("missing home")
|
||||
},
|
||||
fallbackDir: "/var/cache/sower",
|
||||
wantDir: "/var/cache/sower",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
gotDir, err := resolveCacheDir(tt.userCache, tt.fallbackDir)
|
||||
if tt.wantErr && err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !tt.wantErr && err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if gotDir != tt.wantDir {
|
||||
t.Fatalf("resolveCacheDir() = %q, want %q", gotDir, tt.wantDir)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
-149
@@ -1,149 +0,0 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
toml "github.com/pelletier/go-toml"
|
||||
"github.com/wweir/sower/util"
|
||||
"github.com/wweir/utils/log"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Address string `toml:"address"`
|
||||
HTTPProxy string `toml:"http_proxy"`
|
||||
DNSServeIP string `toml:"dns_serve_ip"`
|
||||
DNSUpstream string `toml:"dns_upstream"`
|
||||
PortForward map[string]string `toml:"port_forward"`
|
||||
|
||||
Router struct {
|
||||
DetectLevel int `toml:"detect_level"`
|
||||
ProxyList []string `toml:"proxy_list"`
|
||||
DirectList []string `toml:"direct_list"`
|
||||
} `toml:"router"`
|
||||
}
|
||||
type Server struct {
|
||||
Upstream string `toml:"upstream"`
|
||||
CertFile string `toml:"cert_file"`
|
||||
KeyFile string `toml:"key_file"`
|
||||
CertEmail string `toml:"cert_email"`
|
||||
}
|
||||
|
||||
var (
|
||||
version, date string
|
||||
|
||||
execFile, _ = os.Executable()
|
||||
execDir, _ = filepath.Abs(filepath.Dir(execFile))
|
||||
// Conf full config, include common and server / client
|
||||
conf = struct {
|
||||
file string
|
||||
Password string `toml:"password"`
|
||||
Client Client `toml:"client"`
|
||||
Server Server `toml:"server"`
|
||||
}{}
|
||||
)
|
||||
|
||||
func Init() (*Client, *Server, string) {
|
||||
beforeInitFlag()
|
||||
defer afterInitFlag()
|
||||
flag.StringVar(&conf.Password, "password", "", "password")
|
||||
flag.StringVar(&conf.Server.Upstream, "s", "", "upstream http service, eg: 127.0.0.1:8080")
|
||||
flag.StringVar(&conf.Server.CertFile, "s_cert", "", "tls cert file, gen cert from letsencrypt if empty")
|
||||
flag.StringVar(&conf.Server.KeyFile, "s_key", "", "tls key file, gen cert from letsencrypt if empty")
|
||||
flag.StringVar(&conf.Client.Address, "c", "", "remote server domain, eg: aa.bb.cc, socks5h://127.0.0.1:1080")
|
||||
flag.StringVar(&conf.Client.HTTPProxy, "http_proxy", ":8080", "http proxy, empty to disable")
|
||||
|
||||
if !flag.Parsed() {
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
defer log.Infow("starting", "config", &conf)
|
||||
if conf.file == "" {
|
||||
return &conf.Client, &conf.Server, conf.Password
|
||||
}
|
||||
|
||||
for i := range loadConfigFns {
|
||||
if err := loadConfigFns[i].fn(); err != nil {
|
||||
log.Fatalw("load config", "config", conf.file, "step", loadConfigFns[i].step, "err", err)
|
||||
}
|
||||
}
|
||||
return &conf.Client, &conf.Server, conf.Password
|
||||
}
|
||||
|
||||
// refreshFns will be executed while init and write new config
|
||||
var loadConfigFns = []struct {
|
||||
step string
|
||||
fn func() error
|
||||
}{{"load_config", func() error {
|
||||
f, err := os.OpenFile(conf.file, os.O_RDONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return toml.NewDecoder(f).Decode(&conf)
|
||||
}}}
|
||||
|
||||
// flushCh to avoid parallel persist
|
||||
var flushCh = make(chan struct{})
|
||||
var flushOnce = sync.Once{}
|
||||
|
||||
// PersistRule persist rule into config file
|
||||
func PersistRule(domain string) {
|
||||
flushOnce.Do(func() {
|
||||
go flushConfDaemon()
|
||||
})
|
||||
|
||||
log.Infow("persist direct rule into config", "domain", domain)
|
||||
conf.Client.Router.DirectList = append(conf.Client.Router.DirectList, domain)
|
||||
select {
|
||||
case flushCh <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
func flushConfDaemon() {
|
||||
for range flushCh {
|
||||
// safe write file
|
||||
if conf.file != "" {
|
||||
f, err := os.OpenFile(conf.file+"~", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
log.Errorw("flush config", "step", "flush", "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
conf.Client.Router.DirectList =
|
||||
util.NewReverseSecSlice(conf.Client.Router.DirectList).Sort().Uniq()
|
||||
|
||||
if err := toml.NewEncoder(f).ArraysWithOneElementPerLine(true).Encode(&conf); err != nil {
|
||||
log.Errorw("flush config", "step", "flush", "err", err)
|
||||
f.Close()
|
||||
continue
|
||||
}
|
||||
f.Close()
|
||||
|
||||
if stat, err := os.Stat(conf.file); err != nil {
|
||||
log.Warnw("get file stat", "file", conf.file, "err", err)
|
||||
} else {
|
||||
// There is no common way to transfer ownership for a file
|
||||
// cross-platform. Drop the ownership support but file mod.
|
||||
if err = os.Chmod(conf.file+"~", stat.Mode()); err != nil {
|
||||
log.Warnw("set file mod", "file", conf.file+"~", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = os.Rename(conf.file+"~", conf.file); err != nil {
|
||||
log.Errorw("flush config", "step", "flush", "err", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// reload config
|
||||
for i := range loadConfigFns {
|
||||
if err := loadConfigFns[i].fn(); err != nil {
|
||||
log.Errorw("flush config", "step", loadConfigFns[i].step, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
// +build darwin
|
||||
|
||||
package conf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/wweir/utils/log"
|
||||
)
|
||||
|
||||
const svcPath = "/Library/LaunchDaemons/sower.plist"
|
||||
const svcFile = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>sower</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/bin/sh</string>
|
||||
<string>-c</string>
|
||||
<string>%s %s</string>
|
||||
</array>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>`
|
||||
|
||||
var (
|
||||
ConfigDir = ""
|
||||
installCmd = ""
|
||||
uninstallFlag = false
|
||||
)
|
||||
|
||||
func beforeInitFlag() {
|
||||
if _, err := os.Stat(execDir + "/sower.toml"); err == nil {
|
||||
ConfigDir = execDir
|
||||
} else {
|
||||
dir, _ := os.UserConfigDir()
|
||||
ConfigDir = filepath.Join("/", dir, "sower")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(ConfigDir + "/sower.toml"); err != nil {
|
||||
flag.StringVar(&conf.file, "f", "", "config file, rewrite all other parameters if set")
|
||||
} else {
|
||||
flag.StringVar(&conf.file, "f", ConfigDir+"/sower.toml", "config file, rewrite all other parameters if set")
|
||||
}
|
||||
|
||||
flag.StringVar(&installCmd, "install", "", "install service with cmd, eg: '-f \""+ConfigDir+"/sower.toml\"'")
|
||||
flag.BoolVar(&uninstallFlag, "uninstall", false, "uninstall service")
|
||||
}
|
||||
|
||||
func afterInitFlag() {
|
||||
switch {
|
||||
case installCmd != "":
|
||||
install()
|
||||
case uninstallFlag:
|
||||
uninstall()
|
||||
default:
|
||||
if conf.Client.DNSServeIP != "" {
|
||||
log.Infow("setting system DNS", "DNS", conf.Client.DNSServeIP)
|
||||
if err := execute("networksetup -listallnetworkservices | grep -v '*' | xargs -I {} " +
|
||||
"networksetup -setdnsservers {} " + conf.Client.DNSServeIP); err != nil {
|
||||
log.Errorf("set DNS fail, please set DNS to %s manually", conf.Client.DNSServeIP)
|
||||
}
|
||||
execute("pkill mDNSResponder")
|
||||
}
|
||||
return
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func install() {
|
||||
if err := ioutil.WriteFile(svcPath, []byte(fmt.Sprintf(svcFile, execFile, installCmd)), 0644); err != nil {
|
||||
log.Fatalw("write service file", "err", err)
|
||||
}
|
||||
|
||||
execute("launchctl unload " + svcPath)
|
||||
if err := execute("launchctl load -wF " + svcPath); err != nil {
|
||||
log.Fatalw("install service", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func uninstall() {
|
||||
execute("launchctl unload " + svcPath)
|
||||
os.Remove(svcPath)
|
||||
os.RemoveAll("/etc/sower")
|
||||
}
|
||||
|
||||
func execute(cmd string) error {
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
out, err := exec.CommandContext(ctx, "sh", "-c", cmd).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cmd: %s, err: %s, output: %s", cmd, err, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
// +build linux
|
||||
|
||||
package conf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/wweir/utils/log"
|
||||
)
|
||||
|
||||
const svcPath = "/etc/systemd/system/sower.service"
|
||||
const svcFile = `[Unit]
|
||||
Description=Sower client service
|
||||
After=network.target
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/tmp
|
||||
ExecStart=%s %s
|
||||
RestartSec=3
|
||||
Restart=on-failure`
|
||||
|
||||
var (
|
||||
ConfigDir = ""
|
||||
installCmd = ""
|
||||
uninstallFlag = false
|
||||
)
|
||||
|
||||
func beforeInitFlag() {
|
||||
if _, err := os.Stat(execDir + "/sower.toml"); err == nil {
|
||||
ConfigDir = execDir
|
||||
|
||||
} else if stat, err := os.Stat("/etc/sower"); err == nil && stat.IsDir() {
|
||||
ConfigDir = "/etc/sower"
|
||||
|
||||
} else {
|
||||
dir, _ := os.UserConfigDir()
|
||||
ConfigDir = filepath.Join("/", dir, "sower")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(ConfigDir + "/sower.toml"); err != nil {
|
||||
flag.StringVar(&conf.file, "f", "", "config file, rewrite all other parameters if set")
|
||||
} else {
|
||||
flag.StringVar(&conf.file, "f", ConfigDir+"/sower.toml", "config file, rewrite all other parameters if set")
|
||||
}
|
||||
|
||||
flag.StringVar(&installCmd, "install", "", "install service with cmd, eg: '-f "+ConfigDir+"/sower.toml'")
|
||||
flag.BoolVar(&uninstallFlag, "uninstall", false, "uninstall service")
|
||||
}
|
||||
|
||||
func afterInitFlag() {
|
||||
switch {
|
||||
case installCmd != "":
|
||||
install()
|
||||
case uninstallFlag:
|
||||
uninstall()
|
||||
default:
|
||||
if conf.Client.DNSServeIP != "" {
|
||||
log.Infow("setting system DNS", "DNS", conf.Client.DNSServeIP)
|
||||
if err := execute(fmt.Sprintf("grep '%s' || sed -i '1inameserver %s' /etc/resolv.conf",
|
||||
conf.Client.DNSServeIP, conf.Client.DNSServeIP)); err != nil {
|
||||
log.Errorf("set DNS fail, please set DNS to %s manually", conf.Client.DNSServeIP)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func install() {
|
||||
if err := ioutil.WriteFile(svcPath, []byte(fmt.Sprintf(svcFile, execFile, installCmd)), 0644); err != nil {
|
||||
log.Fatalw("write service file", "err", err)
|
||||
}
|
||||
if err := execute("systemctl daemon-reload"); err != nil {
|
||||
log.Fatalw("install service", "err", err)
|
||||
}
|
||||
if err := execute("systemctl enable sower"); err != nil {
|
||||
log.Fatalw("install service", "err", err)
|
||||
}
|
||||
if err := execute("systemctl start sower"); err != nil {
|
||||
log.Fatalw("install service", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func uninstall() {
|
||||
execute("systemctl stop sower")
|
||||
execute("systemctl disable sower")
|
||||
os.Remove(svcPath)
|
||||
os.RemoveAll("/etc/sower")
|
||||
}
|
||||
|
||||
func execute(cmd string) error {
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
out, err := exec.CommandContext(ctx, "sh", "-c", cmd).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cmd: %s, err: %s, output: %s", cmd, err, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
// +build windows
|
||||
|
||||
package conf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/wweir/utils/log"
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.org/x/sys/windows/svc/eventlog"
|
||||
"golang.org/x/sys/windows/svc/mgr"
|
||||
)
|
||||
|
||||
const name = "sower"
|
||||
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue
|
||||
|
||||
var (
|
||||
ConfigDir = ""
|
||||
installCmd = false
|
||||
uninstallFlag = false
|
||||
)
|
||||
|
||||
func beforeInitFlag() {
|
||||
flag.StringVar(&conf.file, "f", filepath.Join(execDir, "sower.toml"), "config file, rewrite all other parameters if set")
|
||||
flag.BoolVar(&installCmd, "install", false, "put any character to install as a service, eg: true")
|
||||
}
|
||||
|
||||
func afterInitFlag() {
|
||||
switch {
|
||||
case installCmd:
|
||||
install()
|
||||
case uninstallFlag:
|
||||
uninstall()
|
||||
default:
|
||||
runAsService()
|
||||
return
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func runAsService() {
|
||||
os.Chdir(filepath.Dir(os.Args[0]))
|
||||
if active, err := svc.IsAnInteractiveSession(); err != nil {
|
||||
log.Fatalf("failed to determine if we are running in an interactive session: %v", err)
|
||||
} else if !active {
|
||||
go func() {
|
||||
elog, err := eventlog.Open(name)
|
||||
if err != nil {
|
||||
log.Fatalw("install service", "err", err)
|
||||
}
|
||||
defer elog.Close()
|
||||
|
||||
if err := svc.Run(name, &myservice{}); err != nil {
|
||||
elog.Error(1, fmt.Sprintf("%s service failed: %v", name, err))
|
||||
log.Fatalw("install service", "err", err)
|
||||
}
|
||||
elog.Info(1, fmt.Sprintf("winsvc.RunAsService: %s service stopped", name))
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func install() {
|
||||
mgrDo(func(m *mgr.Mgr) error {
|
||||
s, err := m.OpenService(name)
|
||||
if err == nil {
|
||||
s.Close()
|
||||
return fmt.Errorf("service %s already exists", name)
|
||||
}
|
||||
s, err = m.CreateService(name, execFile, mgr.Config{
|
||||
DisplayName: "Sower Proxy",
|
||||
StartType: windows.SERVICE_AUTO_START,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
err = eventlog.InstallAsEventCreate(name, eventlog.Error|eventlog.Warning|eventlog.Info)
|
||||
if err != nil {
|
||||
s.Delete()
|
||||
return fmt.Errorf("SetupEventLogSource() failed: %s", err)
|
||||
}
|
||||
|
||||
return s.Start()
|
||||
})
|
||||
}
|
||||
|
||||
func uninstall() {
|
||||
serviceDo(func(s *mgr.Service) error {
|
||||
err := s.Delete()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return eventlog.Remove(name)
|
||||
})
|
||||
}
|
||||
|
||||
func serviceDo(fn func(*mgr.Service) error) {
|
||||
mgrDo(func(m *mgr.Mgr) error {
|
||||
s, err := m.OpenService(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not access service: %v", err)
|
||||
}
|
||||
defer s.Close()
|
||||
return fn(s)
|
||||
})
|
||||
}
|
||||
func mgrDo(fn func(m *mgr.Mgr) error) {
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
log.Fatalw("install service", "err", err)
|
||||
}
|
||||
defer m.Disconnect()
|
||||
|
||||
if err := fn(m); err != nil {
|
||||
log.Fatalw("install service", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
type myservice struct{}
|
||||
|
||||
func (m *myservice) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
|
||||
elog, err := eventlog.Open(name)
|
||||
if err != nil {
|
||||
log.Errorw("install service", "err", err)
|
||||
return
|
||||
}
|
||||
defer elog.Close()
|
||||
elog.Info(1, strings.Join(args, "-"))
|
||||
|
||||
changes <- svc.Status{State: svc.StartPending}
|
||||
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
||||
for {
|
||||
c := <-r
|
||||
switch c.Cmd {
|
||||
case svc.Interrogate:
|
||||
changes <- c.CurrentStatus
|
||||
// Testing deadlock from https://code.google.com/p/winsvc/issues/detail?id=4
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
changes <- c.CurrentStatus
|
||||
case svc.Stop, svc.Shutdown:
|
||||
changes <- svc.Status{State: svc.StopPending}
|
||||
return
|
||||
case svc.Pause:
|
||||
changes <- svc.Status{State: svc.Paused, Accepts: cmdsAccepted}
|
||||
case svc.Continue:
|
||||
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
||||
default:
|
||||
elog.Error(1, fmt.Sprintf("unexpected control request #%d", c))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func execute(cmd string) error {
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var cmds []string
|
||||
for _, cmd := range strings.Split(cmd, " ") {
|
||||
if cmd == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(cmd, "/") {
|
||||
cmd = strings.Replace(cmd, "/", "-", 1)
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
if len(cmds) != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
command := exec.CommandContext(ctx, cmds[0], cmds[1:]...)
|
||||
command.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
if out, err := command.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("cmd: %s, output: %s, err: %w", cmd, out, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
password="" # sower password
|
||||
|
||||
[client]
|
||||
address = "" # aa.bb.cc, socks5h://127.0.0.1:1080
|
||||
http_proxy=":8080"
|
||||
dns_serve_ip="" # eg: 127.0.0.1. keep empty to disable DNS solution
|
||||
dns_upstream="" # keep empty to set via dhcp, not effective in any environment
|
||||
|
||||
[client.port_forward]
|
||||
# eg: ":2222"="aa.bb.cc:22"
|
||||
|
||||
[client.router]
|
||||
detect_level = 0 # [-4, 4], the bigger the harder to add
|
||||
direct_list = [
|
||||
"**.in-addr.arpa",
|
||||
"imap.*.*",
|
||||
"imap.*.*.*",
|
||||
"smtp.*.*",
|
||||
"smtp.*.*.*",
|
||||
"pop.*.*",
|
||||
"pop.*.*.*",
|
||||
"**.cn",
|
||||
]
|
||||
proxy_list = [
|
||||
"**.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",
|
||||
"*.cloudfront.net",
|
||||
"**.amazon.com",
|
||||
"**.amazonaws.com",
|
||||
"*.githubusercontent.com",
|
||||
"*.githubassets.com",
|
||||
"*.github.*",
|
||||
]
|
||||
|
||||
|
||||
[server]
|
||||
cert_email = "" # eg: user@aa.bb.cc
|
||||
cert_file = "" # eg: /etc/ssl/server.crt
|
||||
key_file = "" # eg: /etc/ssl/server.key
|
||||
upstream = "" # eg: 127.0.0.1:8080
|
||||
@@ -0,0 +1,132 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cristalhq/aconfig"
|
||||
"github.com/cristalhq/aconfig/aconfigtoml"
|
||||
"github.com/cristalhq/aconfig/aconfigyaml"
|
||||
)
|
||||
|
||||
func TestLoadExampleSowerConfigFiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
file string
|
||||
}{
|
||||
{name: "toml", file: "sower.toml"},
|
||||
{name: "yaml", file: "sower.yaml"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var cfg SowerConfig
|
||||
if err := loadConfigFileForTest(tt.file, &cfg); err != nil {
|
||||
t.Fatalf("load config file: %v", err)
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("validate config: %v", err)
|
||||
}
|
||||
if cfg.Remote.TLS.ServerName != "" {
|
||||
t.Fatalf("unexpected remote tls server name: %q", cfg.Remote.TLS.ServerName)
|
||||
}
|
||||
if cfg.DNS.Serve != "127.0.0.1" {
|
||||
t.Fatalf("unexpected dns serve: %q", cfg.DNS.Serve)
|
||||
}
|
||||
if cfg.DNS.ServeIface != "" {
|
||||
t.Fatalf("unexpected dns serve iface: %q", cfg.DNS.ServeIface)
|
||||
}
|
||||
if cfg.Socks5.Addr != "127.0.0.1:1080" {
|
||||
t.Fatalf("unexpected socks5 addr: %q", cfg.Socks5.Addr)
|
||||
}
|
||||
if cfg.Router.Block.FilePrefix != "**." {
|
||||
t.Fatalf("unexpected block file prefix: %q", cfg.Router.Block.FilePrefix)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadExampleSowerdConfigFiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
ext string
|
||||
}{
|
||||
{
|
||||
name: "toml",
|
||||
content: strings.ReplaceAll(
|
||||
ExampleSowerdConfigTOML,
|
||||
`fake_site = "/var/www" # Fake site directory or address (default: /var/www)`,
|
||||
`fake_site = "127.0.0.1:80" # Fake site address for tests`,
|
||||
),
|
||||
ext: ".toml",
|
||||
},
|
||||
{
|
||||
name: "yaml",
|
||||
content: strings.ReplaceAll(
|
||||
mustReadFileForTest(t, "sowerd.yaml"),
|
||||
`fake_site: "/var/www" # Fake site directory or address (default: /var/www)`,
|
||||
`fake_site: "127.0.0.1:80" # Fake site address for tests`,
|
||||
),
|
||||
ext: ".yaml",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
path := filepath.Join(t.TempDir(), "sowerd"+tt.ext)
|
||||
if err := os.WriteFile(path, []byte(tt.content), 0o644); err != nil {
|
||||
t.Fatalf("write temp config: %v", err)
|
||||
}
|
||||
|
||||
var cfg SowerdConfig
|
||||
if err := loadConfigFileForTest(path, &cfg); err != nil {
|
||||
t.Fatalf("load config file: %v", err)
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("validate config: %v", err)
|
||||
}
|
||||
if cfg.ServeIP != "0.0.0.0" {
|
||||
t.Fatalf("unexpected serve ip: %q", cfg.ServeIP)
|
||||
}
|
||||
if cfg.FakeSite != "127.0.0.1:80" {
|
||||
t.Fatalf("unexpected fake site: %q", cfg.FakeSite)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfigFileForTest(path string, dst any) error {
|
||||
return aconfig.LoaderFor(dst, aconfig.Config{
|
||||
SkipEnv: true,
|
||||
SkipFlags: true,
|
||||
AllowUnknownFields: false,
|
||||
Files: []string{path},
|
||||
FileDecoders: map[string]aconfig.FileDecoder{
|
||||
".yaml": aconfigyaml.New(),
|
||||
".toml": aconfigtoml.New(),
|
||||
},
|
||||
}).Load()
|
||||
}
|
||||
|
||||
func mustReadFileForTest(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read file %s: %v", path, err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/sower-proxy/sower/pkg/upstreamtls"
|
||||
)
|
||||
|
||||
type RemoteTLSConfig struct {
|
||||
ServerName string `usage:"override upstream TLS server name (SNI)"`
|
||||
ClientHello string `usage:"uTLS client hello: chrome, firefox, ios, android, edge, safari, 360, qq, randomized, randomized_alpn, randomized_no_alpn, golang"`
|
||||
InsecureSkipVerify bool `default:"false" usage:"skip upstream TLS certificate verification"`
|
||||
}
|
||||
|
||||
type RemoteConfig struct {
|
||||
Type string `default:"sower" required:"true" usage:"option: sower/trojan/socks5"`
|
||||
Addr string `required:"true" usage:"proxy address, eg: proxy.com or proxy.com:443"`
|
||||
Password string `usage:"remote proxy password"`
|
||||
TLS RemoteTLSConfig `flag:"tls"`
|
||||
}
|
||||
|
||||
// SowerConfig represents the configuration for sower client
|
||||
type SowerConfig struct {
|
||||
LogLevel slog.Level `default:"info" usage:"log level: debug, info, warn, error"`
|
||||
|
||||
Remote RemoteConfig
|
||||
|
||||
DNS struct {
|
||||
Disable bool `default:"false" usage:"disable DNS proxy"`
|
||||
Serve string `usage:"dns server ip"`
|
||||
Serve6 string `usage:"dns server ipv6, eg: ::1"`
|
||||
ServeIface string `usage:"use the IP in the net interface, if serve ip not setted. eg: eth0"`
|
||||
Upstream string `usage:"upstream dns server"`
|
||||
Fallback string `default:"223.5.5.5" required:"true" usage:"fallback dns server"`
|
||||
}
|
||||
Socks5 struct {
|
||||
Disable bool `default:"false" usage:"disable sock5 proxy"`
|
||||
Addr string `default:"127.0.0.1:1080" usage:"socks5 listen address"`
|
||||
} `flag:"socks5"`
|
||||
|
||||
Router struct {
|
||||
Block struct {
|
||||
File string `usage:"block list file, local file or remote"`
|
||||
FilePrefix string `default:"**." usage:"parsed as '<prefix>line_text'"`
|
||||
Rules []string `usage:"block list rules"`
|
||||
}
|
||||
Direct struct {
|
||||
File string `usage:"direct list file, local file or remote"`
|
||||
FilePrefix string `default:"**." usage:"parsed as '<prefix>line_text'"`
|
||||
Rules []string `usage:"direct list rules"`
|
||||
}
|
||||
Proxy struct {
|
||||
File string `usage:"proxy list file, local file or remote"`
|
||||
FilePrefix string `default:"**." usage:"parsed as '<prefix>line_text'"`
|
||||
Rules []string `usage:"proxy list rules"`
|
||||
}
|
||||
|
||||
Country struct {
|
||||
MMDB string `usage:"mmdb file"`
|
||||
File string `usage:"CIDR block list file, local file or remote"`
|
||||
FilePrefix string `default:"" usage:"parsed as '<prefix>line_text'"`
|
||||
Rules []string `usage:"CIDR list rules"`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate implements the validation interface for SowerConfig
|
||||
func (c *SowerConfig) Validate() error {
|
||||
switch c.Remote.Type {
|
||||
case "sower", "trojan", "socks5":
|
||||
default:
|
||||
return fmt.Errorf("unsupported remote type %q", c.Remote.Type)
|
||||
}
|
||||
|
||||
remoteHost, err := validateRemoteAddr(c.Remote.Type, c.Remote.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.Remote.TLS.ClientHello != "" {
|
||||
if err := upstreamtls.ValidateClientHello(c.Remote.TLS.ClientHello); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.DNS.ServeIface != "" {
|
||||
iface, err := net.InterfaceByName(c.DNS.ServeIface)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get interface: %w", err)
|
||||
}
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get interface addresses: %w", err)
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
ip, _, err := net.ParseCIDR(addr.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse interface IP: %w", err)
|
||||
}
|
||||
|
||||
if ip.To4() != nil { // ipv4
|
||||
if c.DNS.Serve == "" {
|
||||
c.DNS.Serve = ip.String()
|
||||
}
|
||||
} else if ip.IsGlobalUnicast() { // ipv6 must be global unicast
|
||||
if c.DNS.Serve6 == "" {
|
||||
c.DNS.Serve6 = ip.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateOptionalIP("dns serve", c.DNS.Serve); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateOptionalIP("dns serve6", c.DNS.Serve6); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateOptionalIP("dns upstream", c.DNS.Upstream); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateRequiredIP("dns fallback", c.DNS.Fallback); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !c.DNS.Disable && c.DNS.Serve == "" {
|
||||
return fmt.Errorf("dns serve ip and serve interface not set")
|
||||
}
|
||||
if !c.Socks5.Disable {
|
||||
if _, _, err := net.SplitHostPort(c.Socks5.Addr); err != nil {
|
||||
return fmt.Errorf("invalid socks5 listen address %q: %w", c.Socks5.Addr, err)
|
||||
}
|
||||
}
|
||||
|
||||
c.Router.Direct.Rules = append(c.Router.Direct.Rules,
|
||||
remoteHost, "**.in-addr.arpa", "**.ip6.arpa")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRemoteAddr(remoteType, addr string) (string, error) {
|
||||
switch remoteType {
|
||||
case "socks5":
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid remote socks5 address %q: %w", addr, err)
|
||||
}
|
||||
if host == "" {
|
||||
return "", fmt.Errorf("invalid remote socks5 address %q", addr)
|
||||
}
|
||||
return host, nil
|
||||
default:
|
||||
host, _, err := splitRemoteAddr(addr, "443")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid remote tls address %q: %w", addr, err)
|
||||
}
|
||||
return host, nil
|
||||
}
|
||||
}
|
||||
|
||||
func splitRemoteAddr(addr, defaultPort string) (string, string, error) {
|
||||
if addr == "" {
|
||||
return "", "", fmt.Errorf("empty address")
|
||||
}
|
||||
if host, port, err := net.SplitHostPort(addr); err == nil {
|
||||
if host == "" || port == "" {
|
||||
return "", "", fmt.Errorf("invalid address %q", addr)
|
||||
}
|
||||
return host, port, nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(addr, "[") || strings.HasSuffix(addr, "]") {
|
||||
return "", "", fmt.Errorf("invalid address %q", addr)
|
||||
}
|
||||
if strings.Count(addr, ":") == 1 {
|
||||
return "", "", fmt.Errorf("missing or invalid port in %q", addr)
|
||||
}
|
||||
return addr, defaultPort, nil
|
||||
}
|
||||
|
||||
func validateOptionalIP(name, value string) error {
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
return validateRequiredIP(name, value)
|
||||
}
|
||||
|
||||
func validateRequiredIP(name, value string) error {
|
||||
if net.ParseIP(value) == nil {
|
||||
return fmt.Errorf("invalid %s %q", name, value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
# Sower configuration example (TOML format)
|
||||
# This is the client configuration for the sower proxy
|
||||
|
||||
# Logging configuration
|
||||
log_level = "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]
|
||||
server_name = "" # Override upstream TLS server name (SNI)
|
||||
client_hello = "" # Optional uTLS fingerprint: chrome, firefox, ios, android, edge, safari, 360, qq, randomized, randomized_alpn, randomized_no_alpn, golang
|
||||
insecure_skip_verify = false # Skip upstream TLS certificate verification
|
||||
|
||||
# DNS configuration
|
||||
[dns]
|
||||
disable = false # Disable DNS proxy
|
||||
serve = "127.0.0.1" # DNS server IP address
|
||||
serve_6 = "" # DNS server IPv6 address (optional); aconfig maps Serve6 -> serve_6
|
||||
serve_iface = "" # 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
|
||||
# aconfig maps Socks5 -> socks_5 for file keys.
|
||||
[socks_5]
|
||||
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)
|
||||
file_prefix = "**." # Prefix for block list rules
|
||||
rules = [] # Additional block rules
|
||||
|
||||
# Direct list rules
|
||||
[router.direct]
|
||||
file = "" # Direct list file path (local or remote)
|
||||
file_prefix = "**." # Prefix for direct list rules
|
||||
rules = [] # Additional direct rules
|
||||
|
||||
# Proxy list rules
|
||||
[router.proxy]
|
||||
file = "" # Proxy list file path (local or remote)
|
||||
file_prefix = "**." # 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
|
||||
file_prefix = "" # Prefix for CIDR rules
|
||||
rules = [] # Additional CIDR rules
|
||||
@@ -0,0 +1,56 @@
|
||||
# Sower configuration example (YAML format)
|
||||
# This is the client configuration for the sower proxy
|
||||
|
||||
# Logging configuration
|
||||
log_level: "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:
|
||||
server_name: "" # Override upstream TLS server name (SNI)
|
||||
client_hello: "" # Optional uTLS fingerprint: chrome, firefox, ios, android, edge, safari, 360, qq, randomized, randomized_alpn, randomized_no_alpn, golang
|
||||
insecure_skip_verify: false # Skip upstream TLS certificate verification
|
||||
|
||||
# DNS configuration
|
||||
dns:
|
||||
disable: false # Disable DNS proxy
|
||||
serve: "127.0.0.1" # DNS server IP address
|
||||
serve_6: "" # DNS server IPv6 address (optional)
|
||||
serve_iface: "" # 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
|
||||
socks_5:
|
||||
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)
|
||||
file_prefix: "**." # Prefix for block list rules
|
||||
rules: [] # Additional block rules
|
||||
|
||||
# Direct list rules
|
||||
direct:
|
||||
file: "" # Direct list file path (local or remote)
|
||||
file_prefix: "**." # Prefix for direct list rules
|
||||
rules: [] # Additional direct rules
|
||||
|
||||
# Proxy list rules
|
||||
proxy:
|
||||
file: "" # Proxy list file path (local or remote)
|
||||
file_prefix: "**." # 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
|
||||
file_prefix: "" # Prefix for CIDR rules
|
||||
rules: [] # Additional CIDR rules
|
||||
@@ -0,0 +1,79 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSowerConfigValidateRejectsInvalidRemoteType(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := SowerConfig{}
|
||||
cfg.Remote.Type = "invalid"
|
||||
cfg.Remote.Addr = "example.com"
|
||||
cfg.DNS.Disable = true
|
||||
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Fatal("expected validation error for invalid remote type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSowerConfigValidateRejectsInvalidSocks5Addr(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := SowerConfig{}
|
||||
cfg.Remote.Type = "sower"
|
||||
cfg.Remote.Addr = "example.com"
|
||||
cfg.DNS.Disable = true
|
||||
cfg.Socks5.Addr = "127.0.0.1"
|
||||
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Fatal("expected validation error for invalid socks5 address")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSowerConfigValidateRejectsInvalidFallbackIP(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := SowerConfig{}
|
||||
cfg.Remote.Type = "sower"
|
||||
cfg.Remote.Addr = "example.com"
|
||||
cfg.DNS.Disable = true
|
||||
cfg.DNS.Fallback = "not-an-ip"
|
||||
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Fatal("expected validation error for invalid fallback IP")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSowerConfigValidateAllowsTLSRemoteWithExplicitPort(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := SowerConfig{}
|
||||
cfg.Remote.Type = "sower"
|
||||
cfg.Remote.Addr = "example.com:8443"
|
||||
cfg.Remote.TLS.ClientHello = "chrome"
|
||||
cfg.DNS.Disable = true
|
||||
cfg.DNS.Fallback = "223.5.5.5"
|
||||
cfg.Socks5.Disable = true
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("validate config: %v", err)
|
||||
}
|
||||
if got := cfg.Router.Direct.Rules[len(cfg.Router.Direct.Rules)-3]; got != "example.com" {
|
||||
t.Fatalf("unexpected direct rule host: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSowerConfigValidateRejectsInvalidTLSClientHello(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := SowerConfig{}
|
||||
cfg.Remote.Type = "trojan"
|
||||
cfg.Remote.Addr = "example.com"
|
||||
cfg.Remote.TLS.ClientHello = "not-supported"
|
||||
cfg.DNS.Disable = true
|
||||
cfg.DNS.Fallback = "223.5.5.5"
|
||||
cfg.Socks5.Disable = true
|
||||
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Fatal("expected validation error for invalid TLS client hello")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"os"
|
||||
)
|
||||
|
||||
const ExampleSowerdConfigTOML = `# Sowerd configuration example (TOML format)
|
||||
# This is the daemon configuration for the sower server
|
||||
|
||||
# Logging configuration
|
||||
log_level = "info" # Log level: debug, info, warn, error
|
||||
|
||||
# Server configuration
|
||||
serve_ip = "0.0.0.0" # IP address to listen on for ports 80 and 443
|
||||
password = "change_me" # Password for client authentication
|
||||
fake_site = "/var/www" # Fake site directory or address (default: /var/www)
|
||||
|
||||
# SSL/TLS certificate configuration
|
||||
[cert]
|
||||
email = "your-email@example.com" # Email for Let's Encrypt certificate
|
||||
cert = "" # Path to custom certificate file (optional)
|
||||
key = "" # Path to custom private key file (optional)
|
||||
`
|
||||
|
||||
// SowerdConfig represents the configuration for sowerd daemon
|
||||
type SowerdConfig struct {
|
||||
LogLevel slog.Level `default:"info" usage:"log level: debug, info, warn, error"`
|
||||
ServeIP string `usage:"listen to port 80 443 of the IP"`
|
||||
Password string `required:"true"`
|
||||
FakeSite string `default:"/var/www" usage:"fake site address or directoy. serving on 127.0.0.1:80 if directory"`
|
||||
|
||||
Cert struct {
|
||||
Email string
|
||||
Cert string
|
||||
Key string
|
||||
}
|
||||
}
|
||||
|
||||
// Validate implements the validation interface for SowerdConfig
|
||||
func (c *SowerdConfig) Validate() error {
|
||||
if c.ServeIP != "" && net.ParseIP(c.ServeIP) == nil {
|
||||
return fmt.Errorf("invalid serve ip: %q", c.ServeIP)
|
||||
}
|
||||
if c.Password == "" {
|
||||
return fmt.Errorf("password is required")
|
||||
}
|
||||
if c.FakeSite == "" {
|
||||
return fmt.Errorf("fake site is required")
|
||||
}
|
||||
|
||||
if (c.Cert.Cert == "") != (c.Cert.Key == "") {
|
||||
return fmt.Errorf("cert and key must be configured together")
|
||||
}
|
||||
|
||||
if c.Cert.Cert != "" {
|
||||
if _, err := os.Stat(c.Cert.Cert); err != nil {
|
||||
return fmt.Errorf("stat cert file: %w", err)
|
||||
}
|
||||
if _, err := os.Stat(c.Cert.Key); err != nil {
|
||||
return fmt.Errorf("stat key file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(c.FakeSite); err == nil {
|
||||
return nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("stat fake site: %w", err)
|
||||
}
|
||||
|
||||
if _, _, err := net.SplitHostPort(c.FakeSite); err != nil {
|
||||
return fmt.Errorf("fake site must be an existing directory or host:port: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
# Sowerd configuration example (TOML format)
|
||||
# This is the daemon configuration for the sower server
|
||||
|
||||
# Logging configuration
|
||||
log_level = "info" # Log level: debug, info, warn, error
|
||||
|
||||
# Server configuration
|
||||
serve_ip = "0.0.0.0" # IP address to listen on for ports 80 and 443
|
||||
password = "change_me" # Password for client authentication
|
||||
fake_site = "/var/www" # Fake site directory or address (default: /var/www)
|
||||
|
||||
# SSL/TLS certificate configuration
|
||||
[cert]
|
||||
email = "your-email@example.com" # Email for Let's Encrypt certificate
|
||||
cert = "" # Path to custom certificate file (optional)
|
||||
key = "" # Path to custom private key file (optional)
|
||||
@@ -0,0 +1,16 @@
|
||||
# Sowerd configuration example (YAML format)
|
||||
# This is the daemon configuration for the sower server
|
||||
|
||||
# Logging configuration
|
||||
log_level: "info" # Log level: debug, info, warn, error
|
||||
|
||||
# Server configuration
|
||||
serve_ip: "0.0.0.0" # IP address to listen on for ports 80 and 443
|
||||
password: "change_me" # Password for client authentication
|
||||
fake_site: "/var/www" # Fake site directory or address (default: /var/www)
|
||||
|
||||
# SSL/TLS certificate configuration
|
||||
cert:
|
||||
email: "your-email@example.com" # Email for Let's Encrypt certificate
|
||||
cert: "" # Path to custom certificate file (optional)
|
||||
key: "" # Path to custom private key file (optional)
|
||||
@@ -0,0 +1,76 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSowerdConfigValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg SowerdConfig
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid remote fake site",
|
||||
cfg: SowerdConfig{
|
||||
ServeIP: "0.0.0.0",
|
||||
Password: "secret",
|
||||
FakeSite: "127.0.0.1:8080",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid serve ip",
|
||||
cfg: SowerdConfig{
|
||||
ServeIP: "bad-ip",
|
||||
Password: "secret",
|
||||
FakeSite: "127.0.0.1:8080",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "missing password",
|
||||
cfg: SowerdConfig{
|
||||
FakeSite: "127.0.0.1:8080",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid fake site address",
|
||||
cfg: SowerdConfig{
|
||||
Password: "secret",
|
||||
FakeSite: "not-an-address",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "partial cert config",
|
||||
cfg: SowerdConfig{
|
||||
Password: "secret",
|
||||
FakeSite: "127.0.0.1:8080",
|
||||
Cert: struct {
|
||||
Email string
|
||||
Cert string
|
||||
Key string
|
||||
}{
|
||||
Cert: "/tmp/cert.pem",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := tt.cfg.Validate()
|
||||
if tt.wantErr && err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !tt.wantErr && err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,37 @@
|
||||
module github.com/wweir/sower
|
||||
module github.com/sower-proxy/sower
|
||||
|
||||
go 1.14
|
||||
go 1.26.1
|
||||
|
||||
require (
|
||||
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.0.1
|
||||
github.com/miekg/dns v1.1.29
|
||||
github.com/pelletier/go-toml 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/wweir/utils v0.0.0-20200401081845-fc35af3737dc
|
||||
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904
|
||||
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4
|
||||
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.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/klauspost/compress v1.17.4 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,97 +1,67 @@
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
|
||||
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
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 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/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/guregu/null v3.4.0+incompatible h1:a4mw37gBO7ypcBlTJeZGuMpSxxFTV9qFfFKgWxQSGaM=
|
||||
github.com/guregu/null v3.4.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM=
|
||||
github.com/influxdata/influxdb v1.7.9/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY=
|
||||
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
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/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
|
||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/libp2p/go-reuseport v0.0.1 h1:7PhkfH73VXfPJYKQ6JwS5I/eVcoyYi9IMNGc6FWpFLw=
|
||||
github.com/libp2p/go-reuseport v0.0.1/go.mod h1:jn6RmB1ufnQwl0Q1f+YxAj8isJgDCQzaaxIFYDhcYEA=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
|
||||
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI=
|
||||
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/ulule/deepcopier v0.0.0-20200117111125-792cfb847af8 h1:iCslye9fWwp1ExDu06BcKM8/gjDRAlX9DBL+T8CjuWU=
|
||||
github.com/ulule/deepcopier v0.0.0-20200117111125-792cfb847af8/go.mod h1:wUZg40sMUnY+1FU5F9rZwwCruLb8h1bHF8HzI09kgok=
|
||||
github.com/wweir/utils v0.0.0-20200401081845-fc35af3737dc h1:XvNZju71btZ0/hammNDOu0Jqv7qi0kR0x9KaXYRKOAY=
|
||||
github.com/wweir/utils v0.0.0-20200401081845-fc35af3737dc/go.mod h1:5PfQ9g8nWDRNkzu2gyjtFo/Xo6+s5ZCpulQADjvGz7M=
|
||||
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.14.1 h1:nYDKopTbvAPq/NrUVZwT15y2lpROBiLLyoRTbXOYWOo=
|
||||
go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904 h1:bXoxMPcSLOq08zI3/c5dEBT6lE4eh+jOh886GHrn6V8=
|
||||
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/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-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY=
|
||||
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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.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/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sower-proxy/sower/config"
|
||||
)
|
||||
|
||||
type ConfirmFunc func(label string) bool
|
||||
|
||||
type installOptions struct {
|
||||
confirm ConfirmFunc
|
||||
geteuid func() int
|
||||
executable func() (string, error)
|
||||
evalSymlinks func(string) (string, error)
|
||||
serviceIsActive func() bool
|
||||
systemctl func(args ...string) error
|
||||
targetBinaryPath string
|
||||
servicePath string
|
||||
configPath string
|
||||
defaultFakeSite string
|
||||
defaultConfigTOML string
|
||||
}
|
||||
|
||||
func InstallService(confirm ConfirmFunc) error {
|
||||
return installService(installOptions{
|
||||
confirm: confirm,
|
||||
geteuid: os.Geteuid,
|
||||
executable: os.Executable,
|
||||
evalSymlinks: filepath.EvalSymlinks,
|
||||
serviceIsActive: serviceIsActive,
|
||||
systemctl: systemctl,
|
||||
targetBinaryPath: "/usr/local/bin/sowerd",
|
||||
servicePath: "/etc/systemd/system/sowerd.service",
|
||||
configPath: "/etc/sower/sowerd.toml",
|
||||
defaultFakeSite: "/var/www",
|
||||
defaultConfigTOML: config.ExampleSowerdConfigTOML,
|
||||
})
|
||||
}
|
||||
|
||||
func installService(opts installOptions) error {
|
||||
if opts.geteuid() != 0 {
|
||||
return fmt.Errorf("installing systemd service requires root privileges, try running with sudo")
|
||||
}
|
||||
|
||||
execPath, err := opts.executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get executable path: %w", err)
|
||||
}
|
||||
execPath, err = opts.evalSymlinks(execPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve executable symlink: %w", err)
|
||||
}
|
||||
|
||||
isUpdate := opts.serviceIsActive()
|
||||
if execPath != opts.targetBinaryPath {
|
||||
label := fmt.Sprintf("Copy binary to %s?", opts.targetBinaryPath)
|
||||
if isUpdate {
|
||||
label = fmt.Sprintf("Update binary at %s?", opts.targetBinaryPath)
|
||||
}
|
||||
if opts.confirm != nil && opts.confirm(label) {
|
||||
if err := copyFile(execPath, opts.targetBinaryPath, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if isUpdate {
|
||||
_ = opts.systemctl("stop", "sowerd")
|
||||
}
|
||||
fmt.Printf(" OK binary copied to %s\n", opts.targetBinaryPath)
|
||||
execPath = opts.targetBinaryPath
|
||||
}
|
||||
}
|
||||
|
||||
if err := writeServiceFile(opts.servicePath, execPath, opts.configPath); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf(" OK service file installed: %s\n", opts.servicePath)
|
||||
|
||||
if err := ensureDefaultFiles(opts.configPath, opts.defaultFakeSite, opts.defaultConfigTOML); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := opts.systemctl("daemon-reload"); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(" OK systemd daemon reloaded")
|
||||
|
||||
if isUpdate {
|
||||
return finishUpdate(opts.confirm, opts.systemctl)
|
||||
}
|
||||
return finishInstall(opts.confirm, opts.systemctl)
|
||||
}
|
||||
|
||||
func finishInstall(confirm ConfirmFunc, systemctlFn func(args ...string) error) error {
|
||||
if confirm == nil || !confirm("Start and enable sowerd service now?") {
|
||||
fmt.Println()
|
||||
fmt.Println("sowerd service installed. Next steps:")
|
||||
fmt.Println(" 1. Edit /etc/sower/sowerd.toml")
|
||||
fmt.Println(" 2. sudo systemctl start sowerd")
|
||||
fmt.Println(" 3. sudo systemctl enable sowerd")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := systemctlFn("enable", "sowerd"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := systemctlFn("start", "sowerd"); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(" OK service enabled and started")
|
||||
return nil
|
||||
}
|
||||
|
||||
func finishUpdate(confirm ConfirmFunc, systemctlFn func(args ...string) error) error {
|
||||
if confirm == nil || !confirm("Restart sowerd service now?") {
|
||||
fmt.Println()
|
||||
fmt.Println("sowerd service updated. To apply:")
|
||||
fmt.Println(" sudo systemctl restart sowerd")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := systemctlFn("restart", "sowerd"); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(" OK service restarted")
|
||||
return nil
|
||||
}
|
||||
|
||||
func serviceIsActive() bool {
|
||||
cmd := exec.Command("systemctl", "is-active", "--quiet", "sowerd")
|
||||
return cmd.Run() == nil
|
||||
}
|
||||
|
||||
func systemctl(args ...string) error {
|
||||
cmd := exec.Command("systemctl", args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("systemctl %s: %w - %s", strings.Join(args, " "), err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFile(src, dst string, mode os.FileMode) error {
|
||||
data, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read binary %s: %w", src, err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||
return fmt.Errorf("create binary directory: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(dst, data, mode); err != nil {
|
||||
return fmt.Errorf("write binary to %s: %w", dst, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeServiceFile(servicePath, execPath, configPath string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(servicePath), 0o755); err != nil {
|
||||
return fmt.Errorf("create service directory: %w", err)
|
||||
}
|
||||
|
||||
serviceContent := fmt.Sprintf(`[Unit]
|
||||
Description=sower server service
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=%s -c %s
|
||||
Restart=always
|
||||
RestartSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`, execPath, configPath)
|
||||
|
||||
if err := os.WriteFile(servicePath, []byte(serviceContent), 0o644); err != nil {
|
||||
return fmt.Errorf("write service file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureDefaultFiles(configPath, fakeSiteDir, defaultConfigTOML string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
|
||||
return fmt.Errorf("create config directory: %w", err)
|
||||
}
|
||||
if err := os.MkdirAll(fakeSiteDir, 0o755); err != nil {
|
||||
return fmt.Errorf("create fake site directory: %w", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
return nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("stat default config path: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, []byte(defaultConfigTOML), 0o644); err != nil {
|
||||
return fmt.Errorf("write default config: %w", err)
|
||||
}
|
||||
fmt.Printf(" OK default config written to %s\n", configPath)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInstallServiceRequiresRoot(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := installService(installOptions{
|
||||
geteuid: func() int { return 1000 },
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "root privileges") {
|
||||
t.Fatalf("installService() error = %v, want root privileges error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallServiceFreshInstall(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
execPath := filepath.Join(tmpDir, "sowerd")
|
||||
if err := os.WriteFile(execPath, []byte("binary"), 0o755); err != nil {
|
||||
t.Fatalf("write executable: %v", err)
|
||||
}
|
||||
|
||||
targetBinaryPath := filepath.Join(tmpDir, "usr", "local", "bin", "sowerd")
|
||||
servicePath := filepath.Join(tmpDir, "etc", "systemd", "system", "sowerd.service")
|
||||
configPath := filepath.Join(tmpDir, "etc", "sower", "sowerd.toml")
|
||||
fakeSiteDir := filepath.Join(tmpDir, "var", "www")
|
||||
|
||||
var prompts []string
|
||||
var systemctlCalls []string
|
||||
confirmAnswers := map[string]bool{
|
||||
"Copy binary to " + targetBinaryPath + "?": true,
|
||||
"Start and enable sowerd service now?": true,
|
||||
}
|
||||
|
||||
err := installService(installOptions{
|
||||
confirm: func(label string) bool {
|
||||
prompts = append(prompts, label)
|
||||
return confirmAnswers[label]
|
||||
},
|
||||
geteuid: func() int { return 0 },
|
||||
executable: func() (string, error) { return execPath, nil },
|
||||
evalSymlinks: func(path string) (string, error) { return path, nil },
|
||||
serviceIsActive: func() bool { return false },
|
||||
systemctl: func(args ...string) error {
|
||||
systemctlCalls = append(systemctlCalls, strings.Join(args, " "))
|
||||
return nil
|
||||
},
|
||||
targetBinaryPath: targetBinaryPath,
|
||||
servicePath: servicePath,
|
||||
configPath: configPath,
|
||||
defaultFakeSite: fakeSiteDir,
|
||||
defaultConfigTOML: `password = "change_me"` + "\n" + `fakeSite = "/var/www"` + "\n",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("installService() error = %v", err)
|
||||
}
|
||||
|
||||
gotBinary, err := os.ReadFile(targetBinaryPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read target binary: %v", err)
|
||||
}
|
||||
if string(gotBinary) != "binary" {
|
||||
t.Fatalf("target binary = %q, want %q", string(gotBinary), "binary")
|
||||
}
|
||||
|
||||
serviceContent, err := os.ReadFile(servicePath)
|
||||
if err != nil {
|
||||
t.Fatalf("read service file: %v", err)
|
||||
}
|
||||
serviceText := string(serviceContent)
|
||||
if !strings.Contains(serviceText, "ExecStart="+targetBinaryPath+" -c "+configPath) {
|
||||
t.Fatalf("service file missing ExecStart, content = %s", serviceText)
|
||||
}
|
||||
|
||||
configContent, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read config file: %v", err)
|
||||
}
|
||||
if string(configContent) == "" {
|
||||
t.Fatal("config file is empty")
|
||||
}
|
||||
|
||||
if info, err := os.Stat(fakeSiteDir); err != nil || !info.IsDir() {
|
||||
t.Fatalf("fake site dir stat err = %v, isDir = %v", err, err == nil && info.IsDir())
|
||||
}
|
||||
|
||||
wantPrompts := []string{
|
||||
"Copy binary to " + targetBinaryPath + "?",
|
||||
"Start and enable sowerd service now?",
|
||||
}
|
||||
if !reflect.DeepEqual(prompts, wantPrompts) {
|
||||
t.Fatalf("prompts = %#v, want %#v", prompts, wantPrompts)
|
||||
}
|
||||
|
||||
wantSystemctlCalls := []string{
|
||||
"daemon-reload",
|
||||
"enable sowerd",
|
||||
"start sowerd",
|
||||
}
|
||||
if !reflect.DeepEqual(systemctlCalls, wantSystemctlCalls) {
|
||||
t.Fatalf("systemctl calls = %#v, want %#v", systemctlCalls, wantSystemctlCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallServiceUpdateWithoutRestart(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
targetBinaryPath := filepath.Join(tmpDir, "usr", "local", "bin", "sowerd")
|
||||
if err := os.MkdirAll(filepath.Dir(targetBinaryPath), 0o755); err != nil {
|
||||
t.Fatalf("mkdir target binary dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(targetBinaryPath, []byte("binary"), 0o755); err != nil {
|
||||
t.Fatalf("write target binary: %v", err)
|
||||
}
|
||||
|
||||
servicePath := filepath.Join(tmpDir, "etc", "systemd", "system", "sowerd.service")
|
||||
configPath := filepath.Join(tmpDir, "etc", "sower", "sowerd.toml")
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
|
||||
t.Fatalf("mkdir config dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(configPath, []byte("password = \"existing\"\n"), 0o644); err != nil {
|
||||
t.Fatalf("write existing config: %v", err)
|
||||
}
|
||||
|
||||
var prompts []string
|
||||
var systemctlCalls []string
|
||||
|
||||
err := installService(installOptions{
|
||||
confirm: func(label string) bool {
|
||||
prompts = append(prompts, label)
|
||||
return false
|
||||
},
|
||||
geteuid: func() int { return 0 },
|
||||
executable: func() (string, error) { return targetBinaryPath, nil },
|
||||
evalSymlinks: func(path string) (string, error) { return path, nil },
|
||||
serviceIsActive: func() bool { return true },
|
||||
systemctl: func(args ...string) error {
|
||||
systemctlCalls = append(systemctlCalls, strings.Join(args, " "))
|
||||
return nil
|
||||
},
|
||||
targetBinaryPath: targetBinaryPath,
|
||||
servicePath: servicePath,
|
||||
configPath: configPath,
|
||||
defaultFakeSite: filepath.Join(tmpDir, "var", "www"),
|
||||
defaultConfigTOML: "password = \"change_me\"\n",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("installService() error = %v", err)
|
||||
}
|
||||
|
||||
configContent, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read config file: %v", err)
|
||||
}
|
||||
if string(configContent) != "password = \"existing\"\n" {
|
||||
t.Fatalf("config content = %q, want existing config to be preserved", string(configContent))
|
||||
}
|
||||
|
||||
wantPrompts := []string{"Restart sowerd service now?"}
|
||||
if !reflect.DeepEqual(prompts, wantPrompts) {
|
||||
t.Fatalf("prompts = %#v, want %#v", prompts, wantPrompts)
|
||||
}
|
||||
|
||||
wantSystemctlCalls := []string{"daemon-reload"}
|
||||
if !reflect.DeepEqual(systemctlCalls, wantSystemctlCalls) {
|
||||
t.Fatalf("systemctl calls = %#v, want %#v", systemctlCalls, wantSystemctlCalls)
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/wweir/sower/conf"
|
||||
"github.com/wweir/sower/proxy"
|
||||
"github.com/wweir/sower/router"
|
||||
"github.com/wweir/sower/transport"
|
||||
)
|
||||
|
||||
func main() {
|
||||
client, server, password := conf.Init()
|
||||
|
||||
switch {
|
||||
case server.Upstream != "":
|
||||
proxy.StartServer(server.Upstream, password, conf.ConfigDir,
|
||||
server.CertFile, server.KeyFile, server.CertEmail)
|
||||
|
||||
case client.Address != "":
|
||||
route := &router.Route{
|
||||
ProxyAddress: client.Address,
|
||||
ProxyPassword: password,
|
||||
DetectLevel: client.Router.DetectLevel,
|
||||
DirectList: client.Router.DirectList,
|
||||
ProxyList: client.Router.ProxyList,
|
||||
PersistFn: conf.PersistRule,
|
||||
}
|
||||
|
||||
if client.HTTPProxy != "" {
|
||||
go proxy.StartHTTPProxy(client.HTTPProxy, client.Address,
|
||||
[]byte(password), route.GenProxyCheck(true))
|
||||
}
|
||||
|
||||
enableDNSSolution := client.DNSServeIP != ""
|
||||
if enableDNSSolution {
|
||||
transport.SetDNS(nil, client.DNSUpstream)
|
||||
go proxy.StartDNS(client.DNSServeIP, client.DNSUpstream,
|
||||
route.GenProxyCheck(false))
|
||||
}
|
||||
|
||||
proxy.StartClient(client.Address, password,
|
||||
client.DNSServeIP, enableDNSSolution,
|
||||
client.PortForward, route.GenProxyCheck(true))
|
||||
|
||||
default:
|
||||
fmt.Println()
|
||||
flag.Usage()
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package dhcp
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"crypto/rand"
|
||||
"net"
|
||||
"runtime"
|
||||
"time"
|
||||
@@ -11,16 +11,18 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var xid = make([]byte, 4)
|
||||
var broadcastAddr, _ = net.ResolveUDPAddr("udp", "255.255.255.255:67")
|
||||
|
||||
func GetDefaultDNSServer() (string, error) {
|
||||
func GetDNSServer() ([]string, error) {
|
||||
iface, err := PickInternetInterface()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "pick interface")
|
||||
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},
|
||||
@@ -29,31 +31,36 @@ func GetDefaultDNSServer() (string, error) {
|
||||
var conn net.PacketConn
|
||||
if runtime.GOOS == "windows" {
|
||||
if conn, err = reuseport.ListenPacket("udp4", iface.IP.String()+":68"); err != nil {
|
||||
return "", errors.Wrap(err, "listen dhcp")
|
||||
return nil, errors.Wrap(err, "listen dhcp")
|
||||
}
|
||||
} else {
|
||||
if conn, err = reuseport.ListenPacket("udp4", "0.0.0.0:68"); err != nil {
|
||||
return "", errors.Wrap(err, "listen dhcp")
|
||||
return nil, errors.Wrap(err, "listen dhcp")
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
conn.SetDeadline(time.Now().Add(3 * time.Second))
|
||||
|
||||
if _, err := conn.WriteTo([]byte(pack), broadcastAddr); err != nil {
|
||||
return "", errors.Wrap(err, "write broadcast")
|
||||
return nil, errors.Wrap(err, "write broadcast")
|
||||
}
|
||||
|
||||
buf := make([]byte, 1500 /*MTU*/)
|
||||
n, _, err := conn.ReadFrom(buf)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "read dhcp offer")
|
||||
return nil, errors.Wrap(err, "read dhcp offer")
|
||||
}
|
||||
|
||||
pack = dhcp4.Packet(buf[:n])
|
||||
dnsBytes := pack.ParseOptions()[dhcp4.OptionDomainNameServer]
|
||||
if len(dnsBytes) < 4 {
|
||||
return "", errors.New("no DNS setting in upstream network device")
|
||||
if len(dnsBytes) < 4 || len(dnsBytes)%4 != 0 {
|
||||
return nil, errors.New("invalid DNS setting in upstream network device")
|
||||
}
|
||||
|
||||
return net.IPv4(dnsBytes[0], dnsBytes[1], dnsBytes[2], dnsBytes[3]).String(), nil
|
||||
ips := []string{}
|
||||
for i := 0; i < len(dnsBytes); i += 4 {
|
||||
ips = append(ips, net.IP(dnsBytes[i:i+4]).String())
|
||||
}
|
||||
|
||||
return ips, nil
|
||||
}
|
||||
@@ -3,11 +3,11 @@ package dhcp_test
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/wweir/sower/dhcp"
|
||||
"github.com/sower-proxy/sower/pkg/dhcp"
|
||||
)
|
||||
|
||||
func Example_dns() {
|
||||
got, err := dhcp.GetDefaultDNSServer()
|
||||
got, err := dhcp.GetDNSServer()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package dhcp
|
||||
@@ -15,13 +16,23 @@ func PickInternetInterface() (*Iface, error) {
|
||||
}
|
||||
|
||||
for i := range ifaces {
|
||||
if ifaces[i].Flags&net.FlagUp == 0 || ifaces[i].Flags&net.FlagLoopback != 0 {
|
||||
continue
|
||||
}
|
||||
if len(ifaces[i].HardwareAddr) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
addrs, _ := ifaces[i].Addrs()
|
||||
addrs, err := ifaces[i].Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if ip := addr.(*net.IPNet).IP.To4(); ip != nil {
|
||||
ipNet, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if ip := ipNet.IP.To4(); ip != nil && ip.IsGlobalUnicast() {
|
||||
return &Iface{ifaces[i].HardwareAddr, ip}, nil
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package dhcp_test
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/wweir/sower/dhcp"
|
||||
"github.com/sower-proxy/sower/pkg/dhcp"
|
||||
)
|
||||
|
||||
func Example_iface() {
|
||||
@@ -0,0 +1,184 @@
|
||||
package suffixtree
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Node struct {
|
||||
*node
|
||||
sep string
|
||||
Count uint64
|
||||
}
|
||||
type node struct {
|
||||
secs []string
|
||||
subNodes []*node
|
||||
indexMap map[string]int
|
||||
}
|
||||
|
||||
func NewNodeFromRules(rules ...string) *Node {
|
||||
n := &Node{&node{}, ".", 0}
|
||||
for i := range rules {
|
||||
n.Add(rules[i])
|
||||
}
|
||||
|
||||
n.GC()
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *node) GC() {
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
|
||||
n.secs = GCSlice(n.secs)
|
||||
n.subNodes = GCSlice(n.subNodes)
|
||||
for i := range n.subNodes {
|
||||
n.subNodes[i].GC()
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Node) String() string {
|
||||
return n.string("", " ")
|
||||
}
|
||||
func (n *node) string(prefix, indent string) (out string) {
|
||||
if n == nil {
|
||||
return
|
||||
}
|
||||
for key, val := range n.subNodes {
|
||||
out += prefix + n.secs[key] + "\n" + val.string(prefix+indent, indent)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (n *Node) trim(item string) string {
|
||||
return strings.ToLower(strings.TrimSuffix(item, n.sep))
|
||||
}
|
||||
|
||||
func (n *Node) Add(item string) {
|
||||
n.Count++
|
||||
n.add(strings.Split(n.trim(item), n.sep))
|
||||
}
|
||||
func (n *node) add(secs []string) {
|
||||
length := len(secs)
|
||||
switch length {
|
||||
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.prepend(sec, nil)
|
||||
default:
|
||||
n.append(sec, nil)
|
||||
}
|
||||
default:
|
||||
sec := secs[length-1]
|
||||
if sec == "**" { // ** is only allowed in the last sec
|
||||
sec = "*"
|
||||
}
|
||||
|
||||
idx := n.index(sec)
|
||||
if idx == -1 {
|
||||
switch sec {
|
||||
case "", "*", "**":
|
||||
idx = n.prepend(sec, &node{})
|
||||
default:
|
||||
idx = n.append(sec, &node{})
|
||||
}
|
||||
|
||||
} else if n.subNodes[idx] == nil {
|
||||
n.subNodes[idx] = &node{}
|
||||
n.subNodes[idx].add([]string{""})
|
||||
}
|
||||
|
||||
n.subNodes[idx].add(secs[:length-1])
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Node) Match(item string) bool {
|
||||
if n == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return n.matchSecs(strings.Split(n.trim(item), n.sep))
|
||||
}
|
||||
|
||||
func (n *node) matchSecs(secs []string) bool {
|
||||
length := len(secs)
|
||||
if length == 0 {
|
||||
if n == nil {
|
||||
return true
|
||||
}
|
||||
return n.index("") != -1 || n.index("**") != -1
|
||||
}
|
||||
|
||||
if idx := n.index(secs[length-1]); idx >= 0 {
|
||||
if n.subNodes[idx].matchSecs(secs[:length-1]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if idx := n.index("*"); idx >= 0 {
|
||||
if n.subNodes[idx].matchSecs(secs[:length-1]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return n.index("**") >= 0
|
||||
}
|
||||
|
||||
// index return the sec index in node, or -1 if not found
|
||||
func (n *node) index(sec string) int {
|
||||
if n == nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package suffixtree_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sower-proxy/sower/pkg/suffixtree"
|
||||
)
|
||||
|
||||
func TestNode_Match(t *testing.T) {
|
||||
type test struct {
|
||||
arg string
|
||||
want bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
node *suffixtree.Node
|
||||
tests []test
|
||||
}{{
|
||||
"simple",
|
||||
suffixtree.NewNodeFromRules("a.wweir.cc", "b.wweir.cc"),
|
||||
[]test{
|
||||
{"a.wweir.cc", true},
|
||||
{"b.wweir.cc", true},
|
||||
},
|
||||
}, {
|
||||
"parent",
|
||||
suffixtree.NewNodeFromRules("wweir.cc", "a.wweir.cc"),
|
||||
[]test{
|
||||
{"wweir.cc", true},
|
||||
{"a.wweir.cc", true},
|
||||
{"b.wweir.cc", false},
|
||||
},
|
||||
}, {
|
||||
"fuzz1",
|
||||
suffixtree.NewNodeFromRules("wweir.cc", "a.wweir.cc", "*.wweir.cc"),
|
||||
[]test{
|
||||
{"wweir.cc", true},
|
||||
{"a.wweir.cc", true},
|
||||
{"b.wweir.cc", true},
|
||||
{"a.b.wweir.cc", false},
|
||||
},
|
||||
}, {
|
||||
"fuzz2",
|
||||
suffixtree.NewNodeFromRules("a.*.cc", "c.wweir.*"),
|
||||
[]test{
|
||||
{"wweir.cc", false},
|
||||
{"a.wweir.cc", true},
|
||||
{"b.wweir.cc", false},
|
||||
{"c.wweir.cc", true},
|
||||
},
|
||||
}, {
|
||||
"fuzz3",
|
||||
suffixtree.NewNodeFromRules("*.*.cc", "iamp.*.*"),
|
||||
[]test{
|
||||
{"wweir.cc", false},
|
||||
{"a.wweir.cc", true},
|
||||
{"b.wweir.cc", true},
|
||||
{"iamp.wweir.cc", true},
|
||||
},
|
||||
}, {
|
||||
"fuzz4",
|
||||
suffixtree.NewNodeFromRules("**.cc", "a.**.com", "**.wweir.*"),
|
||||
[]test{
|
||||
{"wweir.cc", true},
|
||||
{"a.wweir.cc", true},
|
||||
{"a.b.wweir.cc", true},
|
||||
{"a.fuzz.com", true},
|
||||
{"b.fuzz.com", false},
|
||||
{"www.wweir.com", true},
|
||||
},
|
||||
}, {
|
||||
"github",
|
||||
suffixtree.NewNodeFromRules("**.github.com"),
|
||||
[]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) {
|
||||
for _, test := range tt.tests {
|
||||
if got := tt.node.Match(test.arg); got != test.want {
|
||||
t.Errorf("Node.Match(%s) = %v, want %v", test.arg, got, test.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package suffixtree
|
||||
|
||||
func GCSlice[T any](arr []T) []T {
|
||||
old := arr
|
||||
return append(make([]T, 0, len(old)), old...)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package upstreamtls
|
||||
|
||||
import (
|
||||
cryptotls "crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
utls "github.com/refraction-networking/utls"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
ServerName string
|
||||
ClientHello string
|
||||
InsecureSkipVerify bool
|
||||
}
|
||||
|
||||
func ValidateClientHello(value string) error {
|
||||
_, err := clientHelloID(value)
|
||||
return err
|
||||
}
|
||||
|
||||
func Dial(dialer *net.Dialer, network, addr string, options Options) (net.Conn, error) {
|
||||
if options.ClientHello == "" {
|
||||
return cryptotls.DialWithDialer(dialer, network, addr, &cryptotls.Config{
|
||||
ServerName: options.ServerName,
|
||||
InsecureSkipVerify: options.InsecureSkipVerify,
|
||||
})
|
||||
}
|
||||
|
||||
helloID, err := clientHelloID(options.ClientHello)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawConn, err := dialer.Dial(network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if timeout := dialer.Timeout; timeout > 0 {
|
||||
if err := rawConn.SetDeadline(time.Now().Add(timeout)); err != nil {
|
||||
rawConn.Close()
|
||||
return nil, fmt.Errorf("set tls deadline: %w", err)
|
||||
}
|
||||
defer rawConn.SetDeadline(time.Time{})
|
||||
}
|
||||
|
||||
serverName := options.ServerName
|
||||
if serverName == "" {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
rawConn.Close()
|
||||
return nil, fmt.Errorf("split tls addr %q: %w", addr, err)
|
||||
}
|
||||
serverName = host
|
||||
}
|
||||
|
||||
conn := utls.UClient(rawConn, &utls.Config{
|
||||
ServerName: serverName,
|
||||
InsecureSkipVerify: options.InsecureSkipVerify,
|
||||
}, helloID)
|
||||
if err := conn.Handshake(); err != nil {
|
||||
rawConn.Close()
|
||||
return nil, fmt.Errorf("utls handshake: %w", err)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func clientHelloID(value string) (utls.ClientHelloID, error) {
|
||||
switch normalizeClientHello(value) {
|
||||
case "chrome":
|
||||
return utls.HelloChrome_Auto, nil
|
||||
case "firefox":
|
||||
return utls.HelloFirefox_Auto, nil
|
||||
case "ios":
|
||||
return utls.HelloIOS_Auto, nil
|
||||
case "android":
|
||||
return utls.HelloAndroid_11_OkHttp, nil
|
||||
case "edge":
|
||||
return utls.HelloEdge_Auto, nil
|
||||
case "safari":
|
||||
return utls.HelloSafari_Auto, nil
|
||||
case "360":
|
||||
return utls.Hello360_Auto, nil
|
||||
case "qq":
|
||||
return utls.HelloQQ_Auto, nil
|
||||
case "randomized":
|
||||
return utls.HelloRandomized, nil
|
||||
case "randomizedalpn":
|
||||
return utls.HelloRandomizedALPN, nil
|
||||
case "randomizednoalpn":
|
||||
return utls.HelloRandomizedNoALPN, nil
|
||||
case "golang":
|
||||
return utls.HelloGolang, nil
|
||||
default:
|
||||
return utls.ClientHelloID{}, fmt.Errorf("unsupported tls client hello %q", value)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeClientHello(value string) string {
|
||||
value = strings.TrimSpace(strings.ToLower(value))
|
||||
value = strings.ReplaceAll(value, "-", "")
|
||||
value = strings.ReplaceAll(value, "_", "")
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package upstreamtls
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestValidateClientHelloAcceptsAliases(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, value := range []string{"chrome", "firefox", "randomized_alpn", "randomized-no-alpn", "golang"} {
|
||||
if err := ValidateClientHello(value); err != nil {
|
||||
t.Fatalf("validate %q: %v", value, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateClientHelloRejectsUnknownValue(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if err := ValidateClientHello("unknown"); err == nil {
|
||||
t.Fatal("expected validation error")
|
||||
}
|
||||
}
|
||||
-118
@@ -1,118 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/wweir/sower/dhcp"
|
||||
"github.com/wweir/utils/log"
|
||||
)
|
||||
|
||||
type msgCache struct {
|
||||
*dns.Msg
|
||||
time.Time
|
||||
}
|
||||
|
||||
var cache sync.Map
|
||||
|
||||
func StartDNS(redirectIP, relayServer string, shouldProxy func(string) bool) {
|
||||
serveIP := net.ParseIP(redirectIP)
|
||||
if redirectIP == "" || serveIP.String() != redirectIP {
|
||||
log.Fatalw("invalid listen ip", "ip", redirectIP)
|
||||
}
|
||||
|
||||
var err error
|
||||
if relayServer, err = pickRelayAddr(relayServer); err != nil {
|
||||
log.Fatalw("pick upstream dns server", "err", err)
|
||||
}
|
||||
log.Infow("upstream dns", "addr", relayServer)
|
||||
|
||||
dns.HandleFunc(".", func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
// *Msg r has an TSIG record and it was validated
|
||||
if r.IsTsig() != nil && w.TsigStatus() == nil {
|
||||
lastTsig := r.Extra[len(r.Extra)-1].(*dns.TSIG)
|
||||
r.SetTsig(lastTsig.Hdr.Name, dns.HmacMD5, 300, time.Now().Unix())
|
||||
}
|
||||
|
||||
//https://stackoverflow.com/questions/4082081/requesting-a-and-aaaa-records-in-single-dns-query/4083071#4083071
|
||||
if len(r.Question) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
domain := r.Question[0].Name
|
||||
if idx := strings.IndexByte(domain, ':'); idx > 0 {
|
||||
domain = domain[:idx] // trim port
|
||||
}
|
||||
|
||||
if shouldProxy(domain) {
|
||||
w.WriteMsg(localA(r, domain, serveIP))
|
||||
|
||||
} else if val, ok := cache.Load(domain); ok && val.(*msgCache).After(time.Now()) {
|
||||
msg := val.(*msgCache)
|
||||
for _, rr := range msg.Answer {
|
||||
rr.Header().Ttl = uint32(time.Until(msg.Time).Seconds())
|
||||
}
|
||||
msg.SetReply(r)
|
||||
w.WriteMsg(msg.Msg)
|
||||
|
||||
} else if msg, err := dns.Exchange(r, relayServer); err != nil || msg == nil {
|
||||
cache.Delete(domain)
|
||||
if server, err := pickRelayAddr(relayServer); err != nil {
|
||||
log.Errorw("detect upstream dns", "err", err)
|
||||
} else if relayServer != server {
|
||||
relayServer = server
|
||||
log.Infow("detect upstream dns", "addr", relayServer)
|
||||
}
|
||||
|
||||
} else {
|
||||
cache.Delete(domain)
|
||||
if len(msg.Answer) != 0 {
|
||||
deadline := time.Now().Add(
|
||||
time.Duration(msg.Answer[0].Header().Ttl) * time.Second)
|
||||
cache.Store(domain, &msgCache{
|
||||
Msg: msg,
|
||||
Time: deadline,
|
||||
})
|
||||
}
|
||||
|
||||
w.WriteMsg(msg)
|
||||
}
|
||||
})
|
||||
|
||||
server := &dns.Server{Addr: net.JoinHostPort(redirectIP, "53"), Net: "udp"}
|
||||
log.Infow("start dns", "addr", server.Addr)
|
||||
log.Fatalw("dns serve fail", "err", server.ListenAndServe())
|
||||
}
|
||||
|
||||
func pickRelayAddr(relayServer string) (_ string, err error) {
|
||||
if relayServer == "" {
|
||||
if relayServer, err = dhcp.GetDefaultDNSServer(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if _, _, err := net.SplitHostPort(relayServer); err != nil {
|
||||
return net.JoinHostPort(relayServer, "53"), nil
|
||||
}
|
||||
return relayServer, nil
|
||||
}
|
||||
|
||||
func localA(r *dns.Msg, domain string, localIP net.IP) *dns.Msg {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
if localIP.To4() != nil {
|
||||
m.Answer = []dns.RR{&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{
|
||||
Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 20},
|
||||
AAAA: localIP,
|
||||
}}
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"time"
|
||||
|
||||
"github.com/wweir/sower/transport"
|
||||
"github.com/wweir/sower/util"
|
||||
"github.com/wweir/utils/log"
|
||||
)
|
||||
|
||||
// StartHTTPProxy start http reverse proxy.
|
||||
// The httputil.ReverseProxy do not supply enough support for https request.
|
||||
func StartHTTPProxy(httpProxyAddr, serverAddr string, password []byte,
|
||||
shouldProxy func(string) bool) {
|
||||
|
||||
proxy := httputil.ReverseProxy{
|
||||
Director: func(r *http.Request) {},
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return transport.Dial(serverAddr, func(host string) (string, []byte) {
|
||||
if shouldProxy(host) {
|
||||
return httpProxyAddr, password
|
||||
}
|
||||
return "", nil
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: httpProxyAddr,
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodConnect {
|
||||
httpsProxy(w, r, serverAddr, password, shouldProxy)
|
||||
} else {
|
||||
proxy.ServeHTTP(w, r)
|
||||
}
|
||||
}),
|
||||
// Disable HTTP/2.
|
||||
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){},
|
||||
IdleTimeout: 90 * time.Second,
|
||||
}
|
||||
|
||||
log.Infow("start sower http proxy", "http_proxy", httpProxyAddr)
|
||||
go log.Fatalw("serve http proxy", "addr", httpProxyAddr, "err", srv.ListenAndServe())
|
||||
}
|
||||
|
||||
func httpsProxy(w http.ResponseWriter, r *http.Request,
|
||||
serverAddr string, password []byte, shouldProxy func(string) bool) {
|
||||
|
||||
conn, _, err := w.(http.Hijacker).Hijack()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
conn.(*net.TCPConn).SetKeepAlive(true)
|
||||
defer conn.Close()
|
||||
|
||||
if _, err := conn.Write([]byte(r.Proto + " 200 Connection established\r\n\r\n")); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
target, _ := util.WithDefaultPort(r.Host, "443")
|
||||
rc, err := transport.Dial(target, func(host string) (string, []byte) {
|
||||
if shouldProxy(host) {
|
||||
return serverAddr, password
|
||||
}
|
||||
return "", nil
|
||||
})
|
||||
if err != nil {
|
||||
conn.Write([]byte("sower dial " + serverAddr + " fail: " + err.Error()))
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
relay(conn, rc)
|
||||
}
|
||||
-141
@@ -1,141 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/wweir/sower/transport"
|
||||
"github.com/wweir/utils/log"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
func StartClient(serverAddr, password, serveIP string, enableDNS bool,
|
||||
forwards map[string]string, shouldProxy func(string) bool) {
|
||||
|
||||
passwordData := []byte(password)
|
||||
relayToRemote := func(lnAddr, target string,
|
||||
parseFn func(net.Conn) (net.Conn, string, error),
|
||||
shouldProxy func(string) bool) {
|
||||
|
||||
ln, err := net.Listen("tcp", lnAddr)
|
||||
if err != nil {
|
||||
log.Fatalw("tcp listen", "port", lnAddr, "err", err)
|
||||
}
|
||||
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Errorw("tcp accept", "port", lnAddr, "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
go func(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
if parseFn != nil {
|
||||
if conn, target, err = parseFn(conn); err != nil {
|
||||
log.Warnw("parse target", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rc, err := transport.Dial(target, func(domain string) (string, []byte) {
|
||||
if shouldProxy(domain) {
|
||||
return serverAddr, passwordData
|
||||
}
|
||||
return "", nil
|
||||
})
|
||||
if err != nil {
|
||||
host, _, _ := net.SplitHostPort(target)
|
||||
log.Warnw("dial", "addr", target, "proxy", shouldProxy(host), "err", err)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
relay(conn, rc)
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
|
||||
for from, to := range forwards {
|
||||
go relayToRemote(from, to, nil, func(string) bool { return true })
|
||||
}
|
||||
|
||||
if enableDNS {
|
||||
go relayToRemote(net.JoinHostPort(serveIP, "http"), "", ParseHTTP, shouldProxy)
|
||||
go relayToRemote(net.JoinHostPort(serveIP, "https"), "", ParseHTTPS, shouldProxy)
|
||||
}
|
||||
|
||||
log.Infow("start sower client", "dns solution", enableDNS, "forwards", forwards)
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
func StartServer(relayTarget, password, cacheDir, certFile, keyFile, email string) {
|
||||
certManager := autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
Email: email,
|
||||
Cache: autocert.DirCache(cacheDir),
|
||||
}
|
||||
|
||||
tlsConf := &tls.Config{
|
||||
GetCertificate: certManager.GetCertificate,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
NextProtos: []string{"http/1.1", "h2"},
|
||||
}
|
||||
if certFile != "" && keyFile != "" {
|
||||
if cert, err := tls.LoadX509KeyPair(certFile, keyFile); err != nil {
|
||||
log.Fatalw("load certificate", "cert", certFile, "key", keyFile, "err", err)
|
||||
} else {
|
||||
tlsConf.GetCertificate = nil
|
||||
tlsConf.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to redirect 80 to 443
|
||||
go http.ListenAndServe(":80", certManager.HTTPHandler(http.HandlerFunc(
|
||||
func(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
|
||||
}
|
||||
|
||||
http.Redirect(w, r, r.URL.String(), 301)
|
||||
})))
|
||||
|
||||
log.Infow("start sower server", "relay_to", relayTarget)
|
||||
ln, err := tls.Listen("tcp", ":443", tlsConf)
|
||||
if err != nil {
|
||||
log.Fatalw("tcp listen", "err", err)
|
||||
}
|
||||
|
||||
passwordData := []byte(password)
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Errorw("tcp accept", "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
go func(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
conn, target := transport.ParseProxyConn(conn, passwordData)
|
||||
if target == "" {
|
||||
target = relayTarget
|
||||
}
|
||||
|
||||
rc, err := net.Dial("tcp", target)
|
||||
if err != nil {
|
||||
log.Errorw("tcp dial", "addr", target, "err", err)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
relay(conn, rc)
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/wweir/sower/util"
|
||||
)
|
||||
|
||||
func ParseHTTP(conn net.Conn) (net.Conn, string, error) {
|
||||
teeConn := &util.TeeConn{Conn: conn}
|
||||
defer teeConn.Stop()
|
||||
|
||||
resp, err := http.ReadRequest(bufio.NewReader(teeConn))
|
||||
if err != nil {
|
||||
return teeConn, "", err
|
||||
}
|
||||
|
||||
resp.Host, _ = util.WithDefaultPort(resp.Host, "80")
|
||||
return teeConn, resp.Host, nil
|
||||
}
|
||||
|
||||
func ParseHTTPS(conn net.Conn) (net.Conn, string, error) {
|
||||
teeConn := &util.TeeConn{Conn: conn}
|
||||
defer teeConn.Stop()
|
||||
|
||||
var domain string
|
||||
tls.Server(teeConn, &tls.Config{
|
||||
GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
domain = hello.ServerName
|
||||
return nil, nil
|
||||
},
|
||||
}).Handshake()
|
||||
|
||||
domain, _ = util.WithDefaultPort(domain, "443")
|
||||
return teeConn, domain, nil
|
||||
}
|
||||
|
||||
func relay(conn1, conn2 net.Conn) {
|
||||
wg := &sync.WaitGroup{}
|
||||
exitFlag := new(int32)
|
||||
wg.Add(2)
|
||||
go redirect(conn2, conn1, wg, exitFlag)
|
||||
redirect(conn1, conn2, wg, exitFlag)
|
||||
wg.Wait()
|
||||
}
|
||||
func redirect(dst, src net.Conn, wg *sync.WaitGroup, exitFlag *int32) {
|
||||
io.Copy(dst, src)
|
||||
|
||||
if atomic.CompareAndSwapInt32(exitFlag, 0, 1) {
|
||||
// wakeup blocked goroutine
|
||||
now := time.Now()
|
||||
src.SetDeadline(now)
|
||||
dst.SetDeadline(now)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (r *Router) localSite(domain string) bool {
|
||||
// parse domain to IP
|
||||
ip := net.ParseIP(domain)
|
||||
if ip == nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
ips, err := net.DefaultResolver.LookupIP(ctx, "ip", domain)
|
||||
if err != nil || len(ips) == 0 {
|
||||
slog.Warn("resolve domain", "error", err, "domain", domain, "ips", len(ips))
|
||||
return false
|
||||
}
|
||||
|
||||
ip = ips[0]
|
||||
}
|
||||
|
||||
// CIDR match
|
||||
for _, cidr := range r.country.cidrs {
|
||||
if cidr.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// MMDB match CN
|
||||
if r.country.Reader != nil {
|
||||
city, err := r.country.City(ip)
|
||||
if err != nil {
|
||||
slog.Warn("mmdb search", "error", err, "domain", domain, "ip", ip)
|
||||
return false
|
||||
}
|
||||
|
||||
if city.Country.IsoCode == "CN" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
+428
@@ -0,0 +1,428 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
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) != 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 )
|
||||
if r.BlockRule.Match(domain) {
|
||||
_ = w.WriteMsg(r.dnsFail(req, dns.RcodeNameError))
|
||||
return
|
||||
}
|
||||
|
||||
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))
|
||||
} else {
|
||||
_ = w.WriteMsg(resp)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) dnsFail(req *dns.Msg, rcode int) *dns.Msg {
|
||||
m := new(dns.Msg)
|
||||
m.SetRcode(req, rcode)
|
||||
return m
|
||||
}
|
||||
|
||||
func (r *Router) dnsNoData(req *dns.Msg) *dns.Msg {
|
||||
return dnsReply(req)
|
||||
}
|
||||
|
||||
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,
|
||||
}, 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)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
addrs, index, shouldProbe, err := r.currentUpstreamState(time.Now())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Port ==========================
|
||||
type Port uint16
|
||||
|
||||
const (
|
||||
HTTP Port = 80
|
||||
HTTPS Port = 443
|
||||
)
|
||||
|
||||
// Ping try connect to a http(s) server with domain though the http addr
|
||||
func (p Port) Ping(domain string, dial func(string) (net.Conn, error)) error {
|
||||
conn, err := dial(net.JoinHostPort(domain, p.String()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
if _, err := conn.Write(p.PingMsg(domain)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// err -> nil: read something succ
|
||||
// err -> io.EOF: no such domain or connection refused
|
||||
// err -> timeout: tcp package has been dropped
|
||||
if _, err = conn.Read(make([]byte, 10)); err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p Port) String() string {
|
||||
return strconv.Itoa(int(p))
|
||||
}
|
||||
|
||||
func (p Port) PingMsg(domain string) []byte {
|
||||
switch p {
|
||||
case HTTP:
|
||||
return []byte("TRACE / HTTP/1.1\r\nHost: " + domain + "\r\n\r\n")
|
||||
case HTTPS:
|
||||
return NewClientHelloSNIMsg(domain)
|
||||
default:
|
||||
panic("invalid port")
|
||||
}
|
||||
}
|
||||
|
||||
// SNI ==========================
|
||||
type clientHelloSNI struct {
|
||||
ContentType uint8
|
||||
Version uint16
|
||||
Length uint16
|
||||
handshakeProtocol
|
||||
}
|
||||
type handshakeProtocol struct {
|
||||
HandshakeType uint8
|
||||
LengthExpand uint8
|
||||
Length uint16
|
||||
Version uint16
|
||||
Random [32]byte
|
||||
SessionIDLength uint8
|
||||
CipherSuitesLength uint16
|
||||
CipherSuite uint16
|
||||
CompressionMethodsLength uint8
|
||||
CompressionMethod uint8
|
||||
ExtensionsLength uint16
|
||||
extensionServerName
|
||||
}
|
||||
type extensionServerName struct {
|
||||
Type uint16
|
||||
Length uint16
|
||||
serverNameIndicationExtension
|
||||
}
|
||||
type serverNameIndicationExtension struct {
|
||||
ServerNameListlength uint16
|
||||
ServerNameType uint8
|
||||
ServerNamelength uint16
|
||||
// ServerName []byte // Disable for fix length
|
||||
}
|
||||
|
||||
func NewClientHelloSNIMsg(domain string) []byte {
|
||||
length := uint16(len(domain))
|
||||
msg := &clientHelloSNI{
|
||||
ContentType: 0x16, // Content Type: Handshake (22)
|
||||
Version: 0x0301, // Version: TLS 1.0
|
||||
Length: length + 56,
|
||||
handshakeProtocol: handshakeProtocol{
|
||||
HandshakeType: 0x01, // Handshake Type: Client Hello (1)
|
||||
Length: length + 52,
|
||||
Version: 0x0303, // Version: TLS 1.2 (0x0303)
|
||||
Random: [32]byte{}, // [32]byte{},
|
||||
SessionIDLength: 0x0, // Session ID Length: 0
|
||||
CipherSuitesLength: 2, // Cipher Suites Length: 84
|
||||
CipherSuite: tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||||
CompressionMethodsLength: 1, // Compression Methods Length: 1
|
||||
CompressionMethod: 0x00, // Compression null
|
||||
ExtensionsLength: length + 9,
|
||||
extensionServerName: extensionServerName{
|
||||
Type: 0x0000, // Type: server_name (0)
|
||||
Length: length + 5,
|
||||
serverNameIndicationExtension: serverNameIndicationExtension{
|
||||
ServerNameListlength: length + 3,
|
||||
ServerNameType: 0x00, // Server Name Type: host_name (0)
|
||||
ServerNamelength: length,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(make([]byte, 0, length+61))
|
||||
binary.Write(buf, binary.BigEndian, msg)
|
||||
buf.WriteString(domain)
|
||||
return buf.Bytes()
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
// +build debug
|
||||
// go test -v -tags debug
|
||||
|
||||
package router_test
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/wweir/sower/router"
|
||||
"github.com/wweir/sower/transport"
|
||||
)
|
||||
|
||||
var (
|
||||
proxyAddr string = "socks5://127.0.0.1:1080"
|
||||
password []byte = nil
|
||||
)
|
||||
|
||||
func TestPort_Ping(t *testing.T) {
|
||||
direct := func(addr string) (net.Conn, error) {
|
||||
return net.DialTimeout("tcp", addr, 5*time.Second)
|
||||
}
|
||||
proxy := func(addr string) (net.Conn, error) {
|
||||
conn, err := tls.Dial("tcp", proxyAddr, &tls.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transport.ToProxyConn(conn, host, uint16(p), password)
|
||||
}
|
||||
|
||||
type args struct {
|
||||
domain string
|
||||
dial func(string) (net.Conn, error)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
p router.Port
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"", router.HTTP, args{"baidu.com", direct}, false},
|
||||
{"", router.HTTP, args{"baidu.com", proxy}, false},
|
||||
{"", router.HTTPS, args{"baidu.com", direct}, false},
|
||||
{"", router.HTTPS, args{"baidu.com", proxy}, false},
|
||||
{"", router.HTTP, args{"google.com", direct}, true},
|
||||
{"", router.HTTP, args{"google.com", proxy}, false},
|
||||
{"", router.HTTPS, args{"google.com", direct}, true},
|
||||
{"", router.HTTPS, args{"google.com", proxy}, false},
|
||||
{"", router.HTTP, args{"smtp.163.com", direct}, true},
|
||||
{"", router.HTTP, args{"smtp.163.com", proxy}, true},
|
||||
{"", router.HTTPS, args{"smtp.163.com", direct}, true},
|
||||
{"", router.HTTPS, args{"smtp.163.com", proxy}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.p.Ping(tt.args.domain, tt.args.dial); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Port.Ping() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sower-proxy/mem"
|
||||
)
|
||||
|
||||
var accessCache = mem.NewRotateCache(time.Hour, httpPing)
|
||||
|
||||
func (r *Router) isAccess(domain string, port uint16) bool {
|
||||
switch port {
|
||||
case 80:
|
||||
case 443:
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
ok, _ := accessCache.Get(accessCacheKey(domain, port))
|
||||
return ok
|
||||
}
|
||||
|
||||
var pingClient = http.Client{
|
||||
Timeout: 2 * time.Second,
|
||||
}
|
||||
|
||||
func httpPing(key string) (bool, error) {
|
||||
scheme, host, err := accessProbeTarget(key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
+115
-117
@@ -1,143 +1,141 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"strings"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/wweir/sower/transport"
|
||||
"github.com/wweir/sower/util"
|
||||
"github.com/wweir/utils/log"
|
||||
"github.com/wweir/utils/mem"
|
||||
geoip2 "github.com/oschwald/geoip2-golang"
|
||||
"github.com/sower-proxy/conns/relay"
|
||||
"github.com/sower-proxy/deferlog/v2"
|
||||
"github.com/sower-proxy/sower/pkg/dhcp"
|
||||
"github.com/sower-proxy/sower/pkg/suffixtree"
|
||||
)
|
||||
|
||||
// Route implement a router for each request
|
||||
type Route struct {
|
||||
once sync.Once
|
||||
cache *mem.Cache
|
||||
type (
|
||||
ProxyDialFn func(network, host string, port uint16) (net.Conn, error)
|
||||
Router struct {
|
||||
BlockRule *suffixtree.Node
|
||||
DirectRule *suffixtree.Node
|
||||
ProxyRule *suffixtree.Node
|
||||
ProxyDial ProxyDialFn
|
||||
|
||||
ProxyAddress string
|
||||
ProxyPassword string
|
||||
password []byte
|
||||
dns struct {
|
||||
upstreamDNS string
|
||||
fallbackDNS string
|
||||
serveIPs []net.IP
|
||||
getDNSServer func() ([]string, error)
|
||||
|
||||
DetectLevel int // dynamic detect proxy level
|
||||
DirectList []string
|
||||
directRule *util.Node
|
||||
ProxyList []string
|
||||
proxyRule *util.Node
|
||||
PersistFn func(string)
|
||||
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
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
var ErrBlocked = errors.New("route blocked")
|
||||
|
||||
func NewRouter(serveIPs []string, upstreamDNS, fallbackDNS, mmdbFile string, proxyDial ProxyDialFn) *Router {
|
||||
r := Router{
|
||||
ProxyDial: proxyDial,
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
slog.Warn("open geoip2 db", "error", err, "file", mmdbFile)
|
||||
}
|
||||
|
||||
return &r
|
||||
}
|
||||
|
||||
// ShouldProxy check if the domain shoule request though proxy
|
||||
func (r *Route) GenProxyCheck(sync bool) func(string) bool {
|
||||
r.once.Do(func() {
|
||||
r.cache = mem.New(4 * time.Hour)
|
||||
r.password = []byte(r.ProxyPassword)
|
||||
r.directRule = util.NewNodeFromRules(r.DirectList...)
|
||||
r.proxyRule = util.NewNodeFromRules(r.ProxyList...)
|
||||
})
|
||||
|
||||
detect := func(domain string) bool {
|
||||
go r.cache.Remember(r, domain)
|
||||
return true
|
||||
}
|
||||
if sync {
|
||||
detect = func(domain string) bool {
|
||||
r.cache.Remember(r, domain)
|
||||
if r.proxyRule.Match(domain) {
|
||||
return true
|
||||
}
|
||||
return !r.directRule.Match(domain)
|
||||
}
|
||||
}
|
||||
|
||||
return func(domain string) bool {
|
||||
domain = strings.TrimSuffix(domain, ".")
|
||||
// break deadlook, for wildcard
|
||||
if sepCount := strings.Count(domain, "."); sepCount == 0 || sepCount >= 5 {
|
||||
return false
|
||||
}
|
||||
|
||||
if r.proxyRule.Match(domain) {
|
||||
return true
|
||||
}
|
||||
if r.directRule.Match(domain) {
|
||||
return false
|
||||
}
|
||||
|
||||
return detect(domain)
|
||||
func (r *Router) AddCountryCIDRs(cidrs ...string) {
|
||||
for _, cidr := range cidrs {
|
||||
_, ipnet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
slog.Error("failed to parse CIDR", "error", err, "cidr", cidr)
|
||||
continue
|
||||
}
|
||||
r.country.cidrs = append(r.country.cidrs, ipnet)
|
||||
}
|
||||
r.country.cidrs = suffixtree.GCSlice(r.country.cidrs)
|
||||
}
|
||||
|
||||
// Get implement for cache
|
||||
func (r *Route) Get(key interface{}) (err error) {
|
||||
domain := key.(string)
|
||||
func (r *Router) RouteHandle(conn net.Conn, domain string, port uint16) (err error) {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
deferlog.DebugWarn(err, "route handle", "domain", domain, "port", port, "took", time.Since(start))
|
||||
}()
|
||||
|
||||
httpScore, httpsScore := r.detect(domain)
|
||||
log.Infow("detect", "domain", domain, "http", httpScore, "https", httpsScore)
|
||||
rc, err := r.Dial("tcp", domain, port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
if httpScore+httpsScore >= r.DetectLevel {
|
||||
r.directRule.Add(domain)
|
||||
if r.PersistFn != nil {
|
||||
r.PersistFn(domain)
|
||||
}
|
||||
if err := relay.Relay(conn, rc); err != nil {
|
||||
return fmt.Errorf("relay %s:%d: %w", domain, port, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// detect and caculate direct connection and proxy connection score
|
||||
func (r *Route) detect(domain string) (http, https int) {
|
||||
wg := sync.WaitGroup{}
|
||||
httpScore, httpsScore := new(int32), new(int32)
|
||||
for _, ping := range [...]struct {
|
||||
shouldProxy bool
|
||||
port Port
|
||||
}{
|
||||
{shouldProxy: true, port: HTTP},
|
||||
{shouldProxy: true, port: HTTPS},
|
||||
{shouldProxy: false, port: HTTP},
|
||||
{shouldProxy: false, port: HTTPS},
|
||||
} {
|
||||
wg.Add(1)
|
||||
go func(shouldProxy bool, port Port) {
|
||||
defer wg.Done()
|
||||
func (r *Router) Dial(network, domain string, port uint16) (net.Conn, error) {
|
||||
addr := net.JoinHostPort(domain, strconv.FormatUint(uint64(port), 10))
|
||||
|
||||
if err := port.Ping(domain, func(domain string) (net.Conn, error) {
|
||||
return transport.Dial(domain,
|
||||
func(domain string) (proxyAddr string, password []byte) {
|
||||
if shouldProxy {
|
||||
return r.ProxyAddress, r.password
|
||||
}
|
||||
return "", nil
|
||||
})
|
||||
}); err != nil {
|
||||
log.Warnw("sower dial", "proxy", shouldProxy, "host", domain, "port", port, "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case shouldProxy && port == HTTP:
|
||||
if !atomic.CompareAndSwapInt32(httpScore, 0, -2) {
|
||||
atomic.AddInt32(httpScore, -1)
|
||||
}
|
||||
case shouldProxy && port == HTTPS:
|
||||
if !atomic.CompareAndSwapInt32(httpsScore, 0, -2) {
|
||||
atomic.AddInt32(httpsScore, -1)
|
||||
}
|
||||
case !shouldProxy && port == HTTP:
|
||||
if !atomic.CompareAndSwapInt32(httpScore, 0, 2) {
|
||||
atomic.AddInt32(httpScore, 1)
|
||||
}
|
||||
case !shouldProxy && port == HTTPS:
|
||||
if !atomic.CompareAndSwapInt32(httpsScore, 0, 2) {
|
||||
atomic.AddInt32(httpsScore, 1)
|
||||
}
|
||||
}
|
||||
}(ping.shouldProxy, ping.port)
|
||||
// 1. rule_based( block > direct > proxy )
|
||||
// 2. detect_based( CN IP || access site )
|
||||
// 3. fallback( proxy )
|
||||
switch {
|
||||
case r.BlockRule.Match(domain):
|
||||
return nil, ErrBlocked
|
||||
case r.DirectRule.Match(domain):
|
||||
return r.directDial(network, addr)
|
||||
case r.ProxyRule.Match(domain):
|
||||
return r.proxyDial(network, domain, port)
|
||||
case r.localSite(domain), r.isAccess(domain, port):
|
||||
return r.directDial(network, addr)
|
||||
default:
|
||||
return r.proxyDial(network, domain, port)
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return int(*httpScore), int(*httpsScore)
|
||||
func (r *Router) proxyDial(network, domain string, port uint16) (net.Conn, error) {
|
||||
start := time.Now()
|
||||
rc, err := r.ProxyDial(network, domain, port)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("proxy dial %s:%d, spend (%s): %w", domain, port, time.Since(start), err)
|
||||
}
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
func (r *Router) directDial(network, addr string) (net.Conn, error) {
|
||||
start := time.Now()
|
||||
conn, err := net.DialTimeout(network, addr, 5*time.Second)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("direct dial %s, spend (%s): %w", addr, time.Since(start), err)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,783 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sower-proxy/mem"
|
||||
"github.com/sower-proxy/sower/pkg/suffixtree"
|
||||
)
|
||||
|
||||
type roundTripFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (fn roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return fn(req)
|
||||
}
|
||||
|
||||
func TestDNSProxyReplyMatchesQuestionType(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1", "::1"}, "", "223.5.5.5", "", nil)
|
||||
|
||||
reqA := new(dns.Msg)
|
||||
reqA.SetQuestion("example.com.", dns.TypeA)
|
||||
respA, err := r.dnsProxyReply("example.com.", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 53}, reqA)
|
||||
if err != nil {
|
||||
t.Fatalf("dnsProxyReply A failed: %v", err)
|
||||
}
|
||||
if len(respA.Answer) != 1 {
|
||||
t.Fatalf("unexpected A answer count: %d", len(respA.Answer))
|
||||
}
|
||||
if _, ok := respA.Answer[0].(*dns.A); !ok {
|
||||
t.Fatalf("expected A record, got %T", respA.Answer[0])
|
||||
}
|
||||
|
||||
reqAAAA := new(dns.Msg)
|
||||
reqAAAA.SetQuestion("example.com.", dns.TypeAAAA)
|
||||
respAAAA, err := r.dnsProxyReply("example.com.", &net.UDPAddr{IP: net.ParseIP("::1"), Port: 53}, reqAAAA)
|
||||
if err != nil {
|
||||
t.Fatalf("dnsProxyReply AAAA failed: %v", err)
|
||||
}
|
||||
if len(respAAAA.Answer) != 1 {
|
||||
t.Fatalf("unexpected AAAA answer count: %d", len(respAAAA.Answer))
|
||||
}
|
||||
if _, ok := respAAAA.Answer[0].(*dns.AAAA); !ok {
|
||||
t.Fatalf("expected AAAA record, got %T", respAAAA.Answer[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSProxyReplyPrefersRequestLocalAddr(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1", "127.0.0.2"}, "", "223.5.5.5", "", nil)
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("example.com.", dns.TypeA)
|
||||
|
||||
resp, err := r.dnsProxyReply("example.com.", &net.UDPAddr{IP: net.ParseIP("127.0.0.2"), Port: 53}, req)
|
||||
if err != nil {
|
||||
t.Fatalf("dnsProxyReply failed: %v", err)
|
||||
}
|
||||
|
||||
answer, ok := resp.Answer[0].(*dns.A)
|
||||
if !ok {
|
||||
t.Fatalf("expected A record, got %T", resp.Answer[0])
|
||||
}
|
||||
if !answer.A.Equal(net.ParseIP("127.0.0.2")) {
|
||||
t.Fatalf("expected request local IP 127.0.0.2, got %s", answer.A)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSProxyReplyIgnoresUnspecifiedLocalAddr(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter([]string{"127.0.0.2", "0.0.0.0"}, "", "223.5.5.5", "", nil)
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("example.com.", dns.TypeA)
|
||||
|
||||
resp, err := r.dnsProxyReply("example.com.", &net.UDPAddr{IP: net.ParseIP("0.0.0.0"), Port: 53}, req)
|
||||
if err != nil {
|
||||
t.Fatalf("dnsProxyReply failed: %v", err)
|
||||
}
|
||||
|
||||
answer, ok := resp.Answer[0].(*dns.A)
|
||||
if !ok {
|
||||
t.Fatalf("expected A record, got %T", resp.Answer[0])
|
||||
}
|
||||
if !answer.A.Equal(net.ParseIP("127.0.0.2")) {
|
||||
t.Fatalf("expected configured serve IP 127.0.0.2, got %s", answer.A)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExchangeSkipsServeIPInUpstreamList(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1"}, "127.0.0.1", "127.0.0.2", "", nil)
|
||||
addrs, err := r.buildUpstreamAddrs()
|
||||
if err != nil {
|
||||
t.Fatalf("buildUpstreamAddrs failed: %v", err)
|
||||
}
|
||||
|
||||
if len(addrs) != 1 {
|
||||
t.Fatalf("unexpected upstream list: %v", addrs)
|
||||
}
|
||||
if addrs[0] != "127.0.0.2:53" {
|
||||
t.Fatalf("expected fallback only, got %v", addrs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildUpstreamAddrsPrefersConfiguredDNSBeforeFallback(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1"}, "1.1.1.1", "9.9.9.9", "", nil)
|
||||
addrs, err := r.buildUpstreamAddrs()
|
||||
if err != nil {
|
||||
t.Fatalf("buildUpstreamAddrs failed: %v", err)
|
||||
}
|
||||
|
||||
want := []string{"1.1.1.1:53", "9.9.9.9:53"}
|
||||
if strings.Join(addrs, ",") != strings.Join(want, ",") {
|
||||
t.Fatalf("unexpected upstream order: got %v want %v", addrs, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddCountryCIDRsSkipsInvalidEntries(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter(nil, "", "223.5.5.5", "", nil)
|
||||
r.AddCountryCIDRs("invalid-cidr")
|
||||
|
||||
if len(r.country.cidrs) != 0 {
|
||||
t.Fatalf("expected invalid CIDR to be skipped, got %d entries", len(r.country.cidrs))
|
||||
}
|
||||
if r.localSite("127.0.0.1") {
|
||||
t.Fatal("unexpected localSite match without valid CIDRs or MMDB")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAccessChecksRequestedPortOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
origTransport := pingClient.Transport
|
||||
origCache := accessCache
|
||||
t.Cleanup(func() {
|
||||
pingClient.Transport = origTransport
|
||||
accessCache = origCache
|
||||
})
|
||||
|
||||
pingClient.Transport = roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
if req.URL.Scheme != "https" {
|
||||
return nil, errors.New("scheme unavailable")
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader("")),
|
||||
Header: make(http.Header),
|
||||
}, nil
|
||||
})
|
||||
accessCache = mem.NewRotateCache(time.Hour, httpPing)
|
||||
|
||||
r := NewRouter(nil, "", "223.5.5.5", "", nil)
|
||||
if !r.isAccess("example.com", 443) {
|
||||
t.Fatal("expected HTTPS probe to succeed")
|
||||
}
|
||||
if r.isAccess("example.com", 80) {
|
||||
t.Fatal("expected HTTP probe to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeDNSForProxyRuleForwardsNonAddressQuestion(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
udpConn, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen udp: %v", err)
|
||||
}
|
||||
defer udpConn.Close()
|
||||
|
||||
handler := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
resp := new(dns.Msg)
|
||||
resp.SetReply(req)
|
||||
resp.Answer = []dns.RR{&dns.TXT{
|
||||
Hdr: dns.RR_Header{Name: req.Question[0].Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 20},
|
||||
Txt: []string{"forwarded"},
|
||||
}}
|
||||
_ = w.WriteMsg(resp)
|
||||
})
|
||||
|
||||
server := &dns.Server{PacketConn: udpConn, Handler: handler}
|
||||
go func() { _ = server.ActivateAndServe() }()
|
||||
t.Cleanup(func() {
|
||||
_ = server.Shutdown()
|
||||
})
|
||||
|
||||
r := NewRouter([]string{"127.0.0.2"}, "", "223.5.5.5", "", nil)
|
||||
r.BlockRule = suffixtree.NewNodeFromRules()
|
||||
r.DirectRule = suffixtree.NewNodeFromRules()
|
||||
r.ProxyRule = suffixtree.NewNodeFromRules("example.com.")
|
||||
r.dns.upstreamAddrs = []string{udpConn.LocalAddr().String()}
|
||||
r.dns.upstreamIndex = 0
|
||||
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("example.com.", dns.TypeTXT)
|
||||
writer := &mockDNSWriter{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.2"), Port: 53}}
|
||||
|
||||
r.ServeDNS(writer, req)
|
||||
|
||||
if writer.msg == nil {
|
||||
t.Fatal("expected response message")
|
||||
}
|
||||
if writer.msg.Rcode != dns.RcodeSuccess {
|
||||
t.Fatalf("expected successful response, got rcode %d", writer.msg.Rcode)
|
||||
}
|
||||
if len(writer.msg.Answer) != 1 {
|
||||
t.Fatalf("expected forwarded TXT answer, got %d answers", len(writer.msg.Answer))
|
||||
}
|
||||
answer, ok := writer.msg.Answer[0].(*dns.TXT)
|
||||
if !ok {
|
||||
t.Fatalf("expected TXT record, got %T", writer.msg.Answer[0])
|
||||
}
|
||||
if len(answer.Txt) != 1 || answer.Txt[0] != "forwarded" {
|
||||
t.Fatalf("unexpected TXT payload: %v", answer.Txt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeDNSForProxyRuleSuppressesHTTPSQuestion(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
udpConn, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen udp: %v", err)
|
||||
}
|
||||
defer udpConn.Close()
|
||||
|
||||
var upstreamQueries atomic.Int32
|
||||
handler := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
upstreamQueries.Add(1)
|
||||
resp := new(dns.Msg)
|
||||
resp.SetReply(req)
|
||||
_ = w.WriteMsg(resp)
|
||||
})
|
||||
|
||||
server := &dns.Server{PacketConn: udpConn, Handler: handler}
|
||||
go func() { _ = server.ActivateAndServe() }()
|
||||
t.Cleanup(func() {
|
||||
_ = server.Shutdown()
|
||||
})
|
||||
|
||||
r := NewRouter([]string{"127.0.0.2"}, "", "223.5.5.5", "", nil)
|
||||
r.BlockRule = suffixtree.NewNodeFromRules()
|
||||
r.DirectRule = suffixtree.NewNodeFromRules()
|
||||
r.ProxyRule = suffixtree.NewNodeFromRules("example.com.")
|
||||
r.dns.upstreamAddrs = []string{udpConn.LocalAddr().String()}
|
||||
r.dns.upstreamIndex = 0
|
||||
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("example.com.", dns.TypeHTTPS)
|
||||
writer := &mockDNSWriter{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.2"), Port: 53}}
|
||||
|
||||
r.ServeDNS(writer, req)
|
||||
|
||||
if writer.msg == nil {
|
||||
t.Fatal("expected response message")
|
||||
}
|
||||
if writer.msg.Rcode != dns.RcodeSuccess {
|
||||
t.Fatalf("expected successful response, got rcode %d", writer.msg.Rcode)
|
||||
}
|
||||
if len(writer.msg.Answer) != 0 {
|
||||
t.Fatalf("expected NODATA response, got %d answers", len(writer.msg.Answer))
|
||||
}
|
||||
if upstreamQueries.Load() != 0 {
|
||||
t.Fatalf("expected HTTPS query to be suppressed locally, got %d upstream queries", upstreamQueries.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeDNSForProxyRuleForwardsSRVQuestion(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
udpConn, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen udp: %v", err)
|
||||
}
|
||||
defer udpConn.Close()
|
||||
|
||||
var upstreamQueries atomic.Int32
|
||||
handler := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
upstreamQueries.Add(1)
|
||||
resp := new(dns.Msg)
|
||||
resp.SetReply(req)
|
||||
resp.Answer = []dns.RR{&dns.SRV{
|
||||
Hdr: dns.RR_Header{Name: req.Question[0].Name, Rrtype: dns.TypeSRV, Class: dns.ClassINET, Ttl: 20},
|
||||
Priority: 10,
|
||||
Weight: 5,
|
||||
Port: 5222,
|
||||
Target: "srv.example.com.",
|
||||
}}
|
||||
_ = w.WriteMsg(resp)
|
||||
})
|
||||
|
||||
server := &dns.Server{PacketConn: udpConn, Handler: handler}
|
||||
go func() { _ = server.ActivateAndServe() }()
|
||||
t.Cleanup(func() {
|
||||
_ = server.Shutdown()
|
||||
})
|
||||
|
||||
r := NewRouter([]string{"127.0.0.2"}, "", "223.5.5.5", "", nil)
|
||||
r.BlockRule = suffixtree.NewNodeFromRules()
|
||||
r.DirectRule = suffixtree.NewNodeFromRules()
|
||||
r.ProxyRule = suffixtree.NewNodeFromRules("example.com.")
|
||||
r.dns.upstreamAddrs = []string{udpConn.LocalAddr().String()}
|
||||
r.dns.upstreamIndex = 0
|
||||
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("_xmpp-client._tcp.example.com.", dns.TypeSRV)
|
||||
writer := &mockDNSWriter{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.2"), Port: 53}}
|
||||
|
||||
r.ServeDNS(writer, req)
|
||||
|
||||
if writer.msg == nil {
|
||||
t.Fatal("expected response message")
|
||||
}
|
||||
if writer.msg.Rcode != dns.RcodeSuccess {
|
||||
t.Fatalf("expected successful response, got rcode %d", writer.msg.Rcode)
|
||||
}
|
||||
if len(writer.msg.Answer) != 1 {
|
||||
t.Fatalf("expected forwarded SRV answer, got %d answers", len(writer.msg.Answer))
|
||||
}
|
||||
answer, ok := writer.msg.Answer[0].(*dns.SRV)
|
||||
if !ok {
|
||||
t.Fatalf("expected SRV record, got %T", writer.msg.Answer[0])
|
||||
}
|
||||
if answer.Target != "srv.example.com." || answer.Port != 5222 {
|
||||
t.Fatalf("unexpected SRV answer: target=%s port=%d", answer.Target, answer.Port)
|
||||
}
|
||||
if upstreamQueries.Load() != 1 {
|
||||
t.Fatalf("expected SRV query to be forwarded once, got %d upstream queries", upstreamQueries.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeDNSForProxyRuleReturnsNoDataWhenAddressFamilyUnavailable(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1"}, "", "223.5.5.5", "", nil)
|
||||
r.BlockRule = suffixtree.NewNodeFromRules()
|
||||
r.DirectRule = suffixtree.NewNodeFromRules()
|
||||
r.ProxyRule = suffixtree.NewNodeFromRules("example.com.")
|
||||
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("example.com.", dns.TypeAAAA)
|
||||
writer := &mockDNSWriter{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 53}}
|
||||
|
||||
r.ServeDNS(writer, req)
|
||||
|
||||
if writer.msg == nil {
|
||||
t.Fatal("expected response message")
|
||||
}
|
||||
if writer.msg.Rcode != dns.RcodeSuccess {
|
||||
t.Fatalf("expected successful response, got rcode %d", writer.msg.Rcode)
|
||||
}
|
||||
if len(writer.msg.Answer) != 0 {
|
||||
t.Fatalf("expected NODATA response, got %d answers", len(writer.msg.Answer))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildUpstreamAddrsIsPerRouter(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r1 := NewRouter([]string{"127.0.0.1"}, "1.1.1.1", "9.9.9.9", "", nil)
|
||||
r2 := NewRouter([]string{"127.0.0.2"}, "8.8.8.8", "4.4.4.4", "", nil)
|
||||
|
||||
addrs1, err := r1.buildUpstreamAddrs()
|
||||
if err != nil {
|
||||
t.Fatalf("buildUpstreamAddrs r1 failed: %v", err)
|
||||
}
|
||||
addrs2, err := r2.buildUpstreamAddrs()
|
||||
if err != nil {
|
||||
t.Fatalf("buildUpstreamAddrs r2 failed: %v", err)
|
||||
}
|
||||
|
||||
if strings.Join(addrs1, ",") == strings.Join(addrs2, ",") {
|
||||
t.Fatalf("expected router-specific upstreams, got %v and %v", addrs1, addrs2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentUpstreamStateSchedulesPreferredProbeAfterRetryInterval(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1"}, "1.1.1.1", "9.9.9.9", "", nil)
|
||||
r.dns.upstreamAddrs = []string{"1.1.1.1:53", "9.9.9.9:53"}
|
||||
r.dns.upstreamIndex = 1
|
||||
r.dns.retryAt = time.Now().Add(-time.Second)
|
||||
|
||||
_, index, shouldProbe, err := r.currentUpstreamState(time.Now())
|
||||
if err != nil {
|
||||
t.Fatalf("currentUpstreamState failed: %v", err)
|
||||
}
|
||||
if index != 1 {
|
||||
t.Fatalf("expected current degraded upstream index 1, got %d", index)
|
||||
}
|
||||
if !shouldProbe {
|
||||
t.Fatal("expected preferred upstream probe to be scheduled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentUpstreamStateAllowsOnlyOnePreferredProbe(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Now()
|
||||
r := NewRouter([]string{"127.0.0.1"}, "1.1.1.1", "9.9.9.9", "", nil)
|
||||
r.dns.upstreamAddrs = []string{"1.1.1.1:53", "9.9.9.9:53"}
|
||||
r.dns.upstreamIndex = 1
|
||||
r.dns.retryAt = now.Add(-time.Second)
|
||||
|
||||
_, _, shouldProbe, err := r.currentUpstreamState(now)
|
||||
if err != nil {
|
||||
t.Fatalf("currentUpstreamState failed: %v", err)
|
||||
}
|
||||
if !shouldProbe {
|
||||
t.Fatal("expected first caller to probe preferred upstream")
|
||||
}
|
||||
|
||||
_, _, shouldProbe, err = r.currentUpstreamState(now)
|
||||
if err != nil {
|
||||
t.Fatalf("currentUpstreamState second call failed: %v", err)
|
||||
}
|
||||
if shouldProbe {
|
||||
t.Fatal("expected second caller to reuse in-flight probe state")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDegradeUpstreamMovesTowardFallback(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1"}, "1.1.1.1", "9.9.9.9", "", nil)
|
||||
r.dns.upstreamAddrs = []string{"1.1.1.1:53", "9.9.9.9:53"}
|
||||
r.dns.upstreamIndex = 0
|
||||
|
||||
index, switched := r.degradeUpstream(0)
|
||||
if !switched {
|
||||
t.Fatal("expected upstream switch")
|
||||
}
|
||||
if index != 1 {
|
||||
t.Fatalf("expected fallback index 1, got %d", index)
|
||||
}
|
||||
if r.dns.retryAt.IsZero() {
|
||||
t.Fatal("expected retry deadline to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromoteUpstreamRestoresPreferredAndClearsRetry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1"}, "1.1.1.1", "9.9.9.9", "", nil)
|
||||
r.dns.upstreamAddrs = []string{"1.1.1.1:53", "9.9.9.9:53"}
|
||||
r.dns.upstreamIndex = 1
|
||||
r.dns.retryAt = time.Now().Add(time.Minute)
|
||||
|
||||
r.promoteUpstream()
|
||||
|
||||
if r.dns.upstreamIndex != 0 {
|
||||
t.Fatalf("expected preferred index 0, got %d", r.dns.upstreamIndex)
|
||||
}
|
||||
if !r.dns.retryAt.IsZero() {
|
||||
t.Fatal("expected retry deadline to be cleared")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentUpstreamStateRefreshesDHCPUpstreams(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var calls atomic.Int32
|
||||
r := NewRouter([]string{"127.0.0.1"}, "", "9.9.9.9", "", nil)
|
||||
r.dns.getDNSServer = func() ([]string, error) {
|
||||
if calls.Add(1) == 1 {
|
||||
return []string{"1.1.1.1"}, nil
|
||||
}
|
||||
return []string{"8.8.8.8"}, nil
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
addrs, index, shouldProbe, err := r.currentUpstreamState(now)
|
||||
if err != nil {
|
||||
t.Fatalf("initial currentUpstreamState failed: %v", err)
|
||||
}
|
||||
if index != 0 || shouldProbe {
|
||||
t.Fatalf("unexpected initial state: index=%d shouldProbe=%v", index, shouldProbe)
|
||||
}
|
||||
if strings.Join(addrs, ",") != "1.1.1.1:53,9.9.9.9:53" {
|
||||
t.Fatalf("unexpected initial upstreams: %v", addrs)
|
||||
}
|
||||
|
||||
addrs, index, shouldProbe, err = r.currentUpstreamState(now.Add(dnsRefreshTTL + time.Second))
|
||||
if err != nil {
|
||||
t.Fatalf("refreshed currentUpstreamState failed: %v", err)
|
||||
}
|
||||
if index != 0 || shouldProbe {
|
||||
t.Fatalf("unexpected refreshed state: index=%d shouldProbe=%v", index, shouldProbe)
|
||||
}
|
||||
if strings.Join(addrs, ",") != "8.8.8.8:53,9.9.9.9:53" {
|
||||
t.Fatalf("unexpected refreshed upstreams: %v", addrs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentUpstreamStateBacksOffInitialDHCPFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var calls atomic.Int32
|
||||
r := NewRouter([]string{"127.0.0.1"}, "", "", "", nil)
|
||||
r.dns.getDNSServer = func() ([]string, error) {
|
||||
calls.Add(1)
|
||||
return nil, errors.New("dhcp unavailable")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if _, _, _, err := r.currentUpstreamState(now); err == nil {
|
||||
t.Fatal("expected initial DHCP failure")
|
||||
}
|
||||
if calls.Load() != 1 {
|
||||
t.Fatalf("expected one DHCP call, got %d", calls.Load())
|
||||
}
|
||||
|
||||
if _, _, _, err := r.currentUpstreamState(now.Add(time.Second)); err == nil {
|
||||
t.Fatal("expected cached DHCP failure during backoff")
|
||||
}
|
||||
if calls.Load() != 1 {
|
||||
t.Fatalf("expected no extra DHCP call during backoff, got %d", calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentUpstreamStateRefreshDoesNotBlockConcurrentReaders(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
startRefresh := make(chan struct{})
|
||||
releaseRefresh := make(chan struct{})
|
||||
var calls atomic.Int32
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1"}, "", "9.9.9.9", "", nil)
|
||||
r.dns.upstreamAddrs = []string{"1.1.1.1:53", "9.9.9.9:53"}
|
||||
r.dns.upstreamIndex = 0
|
||||
r.dns.refreshAt = time.Now().Add(-time.Second)
|
||||
r.dns.getDNSServer = func() ([]string, error) {
|
||||
if calls.Add(1) == 1 {
|
||||
close(startRefresh)
|
||||
<-releaseRefresh
|
||||
}
|
||||
return []string{"8.8.8.8"}, nil
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_, _, _, _ = r.currentUpstreamState(time.Now())
|
||||
}()
|
||||
|
||||
<-startRefresh
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
addrs, index, shouldProbe, err := r.currentUpstreamState(time.Now())
|
||||
if err != nil {
|
||||
t.Errorf("concurrent currentUpstreamState failed: %v", err)
|
||||
return
|
||||
}
|
||||
if index != 0 || shouldProbe {
|
||||
t.Errorf("unexpected concurrent state: index=%d shouldProbe=%v", index, shouldProbe)
|
||||
return
|
||||
}
|
||||
if strings.Join(addrs, ",") != "1.1.1.1:53,9.9.9.9:53" {
|
||||
t.Errorf("unexpected concurrent upstreams: %v", addrs)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
t.Fatal("concurrent reader blocked on refresh lock")
|
||||
}
|
||||
|
||||
close(releaseRefresh)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestCurrentUpstreamStateUsesFallbackWhileInitialRefreshInFlight(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
startRefresh := make(chan struct{})
|
||||
releaseRefresh := make(chan struct{})
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1"}, "", "9.9.9.9", "", nil)
|
||||
r.dns.getDNSServer = func() ([]string, error) {
|
||||
close(startRefresh)
|
||||
<-releaseRefresh
|
||||
return []string{"1.1.1.1"}, nil
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_, _, _, _ = r.currentUpstreamState(time.Now())
|
||||
}()
|
||||
|
||||
<-startRefresh
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
addrs, index, shouldProbe, err := r.currentUpstreamState(time.Now())
|
||||
if err != nil {
|
||||
t.Errorf("currentUpstreamState during initial refresh failed: %v", err)
|
||||
return
|
||||
}
|
||||
if index != 0 || shouldProbe {
|
||||
t.Errorf("unexpected fallback state: index=%d shouldProbe=%v", index, shouldProbe)
|
||||
return
|
||||
}
|
||||
if strings.Join(addrs, ",") != "9.9.9.9:53" {
|
||||
t.Errorf("expected fallback-only upstreams, got %v", addrs)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
t.Fatal("concurrent caller blocked instead of using fallback")
|
||||
}
|
||||
|
||||
close(releaseRefresh)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestCurrentUpstreamStatePreservesProbeWhenRefreshIsDue(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Now()
|
||||
r := NewRouter([]string{"127.0.0.1"}, "", "9.9.9.9", "", nil)
|
||||
r.dns.upstreamAddrs = []string{"1.1.1.1:53", "9.9.9.9:53"}
|
||||
r.dns.upstreamIndex = 1
|
||||
r.dns.retryAt = now.Add(-time.Second)
|
||||
r.dns.refreshAt = now.Add(-time.Second)
|
||||
r.dns.getDNSServer = func() ([]string, error) {
|
||||
return []string{"8.8.8.8"}, nil
|
||||
}
|
||||
|
||||
addrs, index, shouldProbe, err := r.currentUpstreamState(now)
|
||||
if err != nil {
|
||||
t.Fatalf("currentUpstreamState with refresh failed: %v", err)
|
||||
}
|
||||
if !shouldProbe {
|
||||
t.Fatal("expected refresh to preserve due preferred probe")
|
||||
}
|
||||
if index != 1 {
|
||||
t.Fatalf("expected degraded upstream to be preserved across refresh, got %d", index)
|
||||
}
|
||||
if strings.Join(addrs, ",") != "8.8.8.8:53,9.9.9.9:53" {
|
||||
t.Fatalf("unexpected refreshed upstreams: %v", addrs)
|
||||
}
|
||||
if !r.dns.probeInFlight {
|
||||
t.Fatal("expected probe to be marked in-flight after refresh")
|
||||
}
|
||||
if !r.dns.retryAt.After(now) {
|
||||
t.Fatalf("expected retryAt to be pushed forward after scheduling probe, got %v", r.dns.retryAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeDNSReturnsNXDomainForBlockedDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1"}, "", "223.5.5.5", "", nil)
|
||||
r.BlockRule = suffixtree.NewNodeFromRules("example.com.")
|
||||
r.DirectRule = suffixtree.NewNodeFromRules()
|
||||
r.ProxyRule = suffixtree.NewNodeFromRules()
|
||||
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("example.com.", dns.TypeA)
|
||||
|
||||
writer := &mockDNSWriter{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 53}}
|
||||
r.ServeDNS(writer, req)
|
||||
|
||||
if writer.msg == nil {
|
||||
t.Fatal("expected response message")
|
||||
}
|
||||
if writer.msg.Rcode != dns.RcodeNameError {
|
||||
t.Fatalf("expected NXDOMAIN, got %d", writer.msg.Rcode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeDNSRejectsMultipleQuestions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := NewRouter([]string{"127.0.0.1"}, "", "223.5.5.5", "", nil)
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("example.com.", dns.TypeA)
|
||||
req.Question = append(req.Question, dns.Question{Name: "example.org.", Qtype: dns.TypeA, Qclass: dns.ClassINET})
|
||||
|
||||
writer := &mockDNSWriter{localAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 53}}
|
||||
r.ServeDNS(writer, req)
|
||||
|
||||
if writer.msg == nil {
|
||||
t.Fatal("expected response message")
|
||||
}
|
||||
if writer.msg.Rcode != dns.RcodeFormatError {
|
||||
t.Fatalf("expected format error, got %d", writer.msg.Rcode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExchangeWithRetryFallsBackToTCPOnTruncatedUDP(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
udpConn, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen udp: %v", err)
|
||||
}
|
||||
defer udpConn.Close()
|
||||
|
||||
tcpLn, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strings.Split(udpConn.LocalAddr().String(), ":")[1]))
|
||||
if err != nil {
|
||||
t.Fatalf("listen tcp: %v", err)
|
||||
}
|
||||
defer tcpLn.Close()
|
||||
|
||||
var tcpQueries atomic.Int32
|
||||
handler := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
resp := new(dns.Msg)
|
||||
resp.SetReply(req)
|
||||
if _, ok := w.RemoteAddr().(*net.TCPAddr); ok {
|
||||
tcpQueries.Add(1)
|
||||
resp.Answer = []dns.RR{&dns.A{
|
||||
Hdr: dns.RR_Header{Name: req.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 20},
|
||||
A: net.ParseIP("127.0.0.1"),
|
||||
}}
|
||||
} else {
|
||||
resp.Truncated = true
|
||||
}
|
||||
_ = w.WriteMsg(resp)
|
||||
})
|
||||
|
||||
udpServer := &dns.Server{PacketConn: udpConn, Handler: handler}
|
||||
tcpServer := &dns.Server{Listener: tcpLn, Handler: handler}
|
||||
go func() { _ = udpServer.ActivateAndServe() }()
|
||||
go func() { _ = tcpServer.ActivateAndServe() }()
|
||||
t.Cleanup(func() {
|
||||
_ = udpServer.Shutdown()
|
||||
_ = tcpServer.Shutdown()
|
||||
})
|
||||
|
||||
r := NewRouter(nil, "", "223.5.5.5", "", nil)
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("example.com.", dns.TypeA)
|
||||
|
||||
resp, err := r.exchangeWithRetry(req, udpConn.LocalAddr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("exchangeWithRetry failed: %v", err)
|
||||
}
|
||||
if len(resp.Answer) != 1 {
|
||||
t.Fatalf("expected tcp answer, got %d answers", len(resp.Answer))
|
||||
}
|
||||
if tcpQueries.Load() == 0 {
|
||||
t.Fatal("expected TCP fallback query")
|
||||
}
|
||||
}
|
||||
|
||||
type mockDNSWriter struct {
|
||||
msg *dns.Msg
|
||||
localAddr net.Addr
|
||||
}
|
||||
|
||||
func (w *mockDNSWriter) LocalAddr() net.Addr { return w.localAddr }
|
||||
func (w *mockDNSWriter) RemoteAddr() net.Addr { return &net.UDPAddr{} }
|
||||
func (w *mockDNSWriter) WriteMsg(msg *dns.Msg) error { w.msg = msg; return nil }
|
||||
func (w *mockDNSWriter) Write([]byte) (int, error) { return 0, nil }
|
||||
func (w *mockDNSWriter) Close() error { return nil }
|
||||
func (w *mockDNSWriter) TsigStatus() error { return nil }
|
||||
func (w *mockDNSWriter) TsigTimersOnly(bool) {}
|
||||
func (w *mockDNSWriter) Hijack() {}
|
||||
@@ -0,0 +1,28 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func ParseHostPort(hostport string, u *url.URL) (string, uint16, error) {
|
||||
host, port, err := net.SplitHostPort(hostport)
|
||||
if err != nil {
|
||||
if err.(*net.AddrError).Err == "missing port in address" {
|
||||
switch u.Scheme {
|
||||
case "http":
|
||||
return hostport, 80, nil
|
||||
case "https":
|
||||
return hostport, 443, nil
|
||||
}
|
||||
}
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
portInt, err := strconv.ParseUint(port, 10, 16)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
return host, uint16(portInt), nil
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="351px" height="491px" viewBox="-0.5 -0.5 351 491" content="<mxfile><diagram id="u2y3ysnPqCVZKoHCRF4D" name="第 1 页">3VlNc9owEP01HJPxB/7gGCBtD2mbGdppcxRY2GpsyyOLAv31XSEZW5ZJKHEIzSWR1itZem/1dmUG7iTbfGSoSD7TCKcDx4o2A3c6cJyRH8JfYdhKQxAE0hAzEkmTXRtm5A9WRktZVyTCpebIKU05KXTjguY5XnDNhhija91tSVP9rQWKsWGYLVBqWn+QiCfSGjpBbf+ESZxUb7b9kXySocpZ7aRMUETXDZN7O3AnjFIuW9lmglOBXYWLHPfhwNP9whjO+TEDxDMx4jdKV2pzamF8W+2W0VUeYTHAGrjjdUI4nhVoIZ6ugV6wJTxLoWdDU02HGcebg2uy9zuFCME0w5xtwUUNcDwFjooOO1D9dY21W/kkDZyHI2VEit94P3cNATQUCt2IDA1APn37dj8zUMERhITqUsYTGtMcpbe1dazjVvvcUVootH5hzrcqvtGKUx1LwIttf4rx117VfWg+m27U5LK3Vb0DgMPy6Yot1PoVzxyxGCsvV5rEzp7kieEUcfJbPwwvwdw2QS/p4rH0wLYqSs4wyt6EgA3hEn/HU92HCnFo1/CLzino9we1GnpPCby1PkqhfpRcv3VCJP1qVE3YDWNo23ArhEN5+D2u1/2emn85Yx0N+z0eFyD+29If/J/0e/556N8L72vRHxj6EBEmMjvYSEl4Z3TcoTmUHRqjKCVxDu0FkIMZGESiIpDYb9SDjESRDB5ckj9ovptP0KoggMm98cCbdhPtH8x+qhJRE2rJvisFWteW7QcapldqplNDonKhy2WJeYuefyXErBpKugZAHT8FLMZz0Yr5DqS2ha1SKN1gPXkkKkDMJY1zVGJhgEMrmOmhBuk4ggYxh2uQlnAObbMG2fs0axDb6qEGscO3kLujlKv3GuGAojg6/k4b1gPKdYq4jN5XbjkXQ6OzMeR2q010cSrhDI9UibAHkQguViNUGmxeLV6n4PnXOsWzPI0vz23djp9RoZb/i+uasCOw5bXHR5kI13xein+wKBhIYrxbXMHoZnsZFU9Vl7244Lmyrl3bt/WKZ/iyiqdaUGtELwWQbzA3Fp+VdiWQRh04fQdzafBVJqgQzVWW3iw4bVKzo/GeQl1LqKBoTjmnWQd3nLY0DYqnlOR4sv/u9WTWOF7o2mK/v1g0P8l06Fz7vnFSMWR+kTprfj4+PYvOPWYEtiiO2vEpe3QuwXxe4ZxX+kDgXpby2mZNMf0yq+4fgotE/M2FUFjLlK7Nc30J+ludjV4EeBiEoQb6Jd04RwZfX4Ei9j7k1W1H+xnltXp1A9klehTBDwBhA1fYEX+9CBc6XEpJFlOXnNFHgDsV/E1zmuOe0lnru5gdmnj7HXi3xfEkvG0Db6X77xlxN/SeRTzoB3Ho1r+fSaWpf4R0b/8C</diagram></mxfile>">
|
||||
<defs/>
|
||||
<g>
|
||||
<rect x="0" y="0" width="350" height="490" fill="#ffffff" stroke="#000000" pointer-events="all"/>
|
||||
<path d="M 170 240 L 170 156.37" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 170 151.12 L 173.5 158.12 L 170 156.37 L 166.5 158.12 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 195px; margin-left: 170px;">
|
||||
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||
<div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; background-color: #ffffff; white-space: nowrap; ">
|
||||
HTTPS
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="170" y="198" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">
|
||||
HTTPS
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<path d="M 100 240 L 100 190 L 36.37 190" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 31.12 190 L 38.12 186.5 L 36.37 190 L 38.12 193.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 190px; margin-left: 90px;">
|
||||
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||
<div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; background-color: #ffffff; white-space: nowrap; ">
|
||||
socks5 upstream
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="90" y="193" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">
|
||||
socks5 upstream
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<path d="M 240 240 L 240 190 L 303.63 190" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 308.88 190 L 301.88 193.5 L 303.63 190 L 301.88 186.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 192px; margin-left: 252px;">
|
||||
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||
<div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; background-color: #ffffff; white-space: nowrap; ">
|
||||
direct visit
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="252" y="195" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">
|
||||
direct visit
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<rect x="30" y="240" width="280" height="100" fill="#ffffff" stroke="#000000" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 278px; height: 1px; padding-top: 290px; margin-left: 31px;">
|
||||
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||
sower
|
||||
<br/>
|
||||
<br/>
|
||||
rules and detect based router
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="170" y="294" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
sower...
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<path d="M 170 70 L 170 36.37" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 170 31.12 L 173.5 38.12 L 170 36.37 L 166.5 38.12 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<path d="M 240 70 L 240 36.37" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 240 31.12 L 243.5 38.12 L 240 36.37 L 236.5 38.12 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<rect x="30" y="70" width="280" height="80" fill="#ffffff" stroke="#000000" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 278px; height: 1px; padding-top: 110px; margin-left: 31px;">
|
||||
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
|
||||
sowerd
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="170" y="114" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
sowerd
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<path d="M 255 390 L 255 360 L 170 360 L 170 346.37" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 170 341.12 L 173.5 348.12 L 170 346.37 L 166.5 348.12 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 357px; margin-left: 251px;">
|
||||
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||
<div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; background-color: #ffffff; white-space: nowrap; ">
|
||||
socks5 intelligent proxy
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="251" y="360" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">
|
||||
socks5 intelligent proxy
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<ellipse cx="255" cy="397.5" rx="7.5" ry="7.5" fill="#ffffff" stroke="#000000" pointer-events="all"/>
|
||||
<path d="M 255 405 L 255 430 M 255 410 L 240 410 M 255 410 L 270 410 M 255 430 L 240 450 M 255 430 L 270 450" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 457px; margin-left: 255px;">
|
||||
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">
|
||||
Browser Users
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="255" y="469" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
Brows...
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<path d="M 85 390 L 85 360 L 170 360 L 170 346.37" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||
<path d="M 170 341.12 L 173.5 348.12 L 170 346.37 L 166.5 348.12 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 360px; margin-left: 91px;">
|
||||
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||
<div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; background-color: #ffffff; white-space: nowrap; ">
|
||||
DNS route the net flow
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="91" y="363" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">
|
||||
DNS route the net flow
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<ellipse cx="85" cy="397.5" rx="7.5" ry="7.5" fill="#ffffff" stroke="#000000" pointer-events="all"/>
|
||||
<path d="M 85 405 L 85 430 M 85 410 L 70 410 M 85 410 L 100 410 M 85 430 L 70 450 M 85 430 L 100 450" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 457px; margin-left: 85px;">
|
||||
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">
|
||||
Other Users
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="85" y="469" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
Other...
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<rect x="210" y="10" width="60" height="20" fill="none" stroke="none" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 20px; margin-left: 240px;">
|
||||
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">
|
||||
fake site
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="240" y="24" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
fake site
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
<rect x="135" y="10" width="70" height="20" fill="none" stroke="none" pointer-events="all"/>
|
||||
<g transform="translate(-0.5 -0.5)">
|
||||
<switch>
|
||||
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 20px; margin-left: 170px;">
|
||||
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
|
||||
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">
|
||||
target site
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text x="170" y="24" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||
target site
|
||||
</text>
|
||||
</switch>
|
||||
</g>
|
||||
</g>
|
||||
<switch>
|
||||
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/>
|
||||
<a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank">
|
||||
<text text-anchor="middle" font-size="10px" x="50%" y="100%">
|
||||
Viewer does not support full SVG 1.1
|
||||
</text>
|
||||
</a>
|
||||
</switch>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 18 KiB |
@@ -1,68 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/wweir/sower/util"
|
||||
)
|
||||
|
||||
// checksum(>=0x80) + port + target_length + target + data
|
||||
// data(HTTP, first byte < 0x7F)
|
||||
type head struct {
|
||||
Checksum byte
|
||||
Port uint16
|
||||
AddrLen uint8
|
||||
}
|
||||
|
||||
func ParseProxyConn(conn net.Conn, password []byte) (net.Conn, string) {
|
||||
teeConn := &util.TeeConn{Conn: conn}
|
||||
defer teeConn.Stop()
|
||||
|
||||
h := &head{}
|
||||
if err := binary.Read(teeConn, binary.BigEndian, h); err != nil || h.Checksum < 0x80 {
|
||||
return teeConn, ""
|
||||
}
|
||||
|
||||
buf := make([]byte, int(h.AddrLen))
|
||||
if _, err := io.ReadFull(teeConn, buf); err != nil {
|
||||
return teeConn, ""
|
||||
}
|
||||
|
||||
if h.Checksum != sumChecksum(buf, password) {
|
||||
return teeConn, ""
|
||||
}
|
||||
|
||||
teeConn.Reset()
|
||||
return teeConn, net.JoinHostPort(string(buf), strconv.Itoa(int(h.Port)))
|
||||
}
|
||||
|
||||
func ToProxyConn(conn net.Conn, tgtHost string, tgtPort uint16, password []byte) (net.Conn, error) {
|
||||
h := &head{
|
||||
Checksum: sumChecksum([]byte(tgtHost), password),
|
||||
Port: tgtPort,
|
||||
AddrLen: uint8(len(tgtHost)),
|
||||
}
|
||||
if err := binary.Write(conn, binary.BigEndian, h); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var data = []byte(tgtHost)
|
||||
var err error
|
||||
for n, nn := 0, 0; nn < len(data); nn += n {
|
||||
if n, err = conn.Write(data[nn:]); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func sumChecksum(target, password []byte) byte {
|
||||
return md5.Sum(append(target, password...))[0] | 0x80
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func IsSocks5Schema(addr string) (string, bool) {
|
||||
if strings.HasPrefix(addr, "socks5://") {
|
||||
return strings.TrimPrefix(addr, "socks5://"), true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(addr, "socks5h://") {
|
||||
return strings.TrimPrefix(addr, "socks5h://"), true
|
||||
}
|
||||
|
||||
return addr, false
|
||||
}
|
||||
|
||||
func ToSocks5(c net.Conn, host string, port uint16) (net.Conn, error) {
|
||||
return &conn{
|
||||
init: make(chan struct{}),
|
||||
Conn: c,
|
||||
domain: host,
|
||||
port: port,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
init chan struct{}
|
||||
domain string
|
||||
port uint16
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (c *conn) Read(b []byte) (n int, err error) {
|
||||
<-c.init
|
||||
return c.Conn.Read(b)
|
||||
}
|
||||
|
||||
func (c *conn) Write(b []byte) (n int, err error) {
|
||||
select {
|
||||
case <-c.init:
|
||||
return c.Conn.Write(b)
|
||||
default:
|
||||
}
|
||||
|
||||
{
|
||||
req := &authReq{
|
||||
VER: 5,
|
||||
NMETHODS: 1,
|
||||
METHODS: [1]byte{0}, // NO AUTHENTICATION REQUIRED
|
||||
}
|
||||
if err := binary.Write(c.Conn, binary.BigEndian, req); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
{
|
||||
resp := &authResp{}
|
||||
if err := binary.Read(c.Conn, binary.BigEndian, resp); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
{
|
||||
portBuf := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(portBuf, c.port)
|
||||
req := &request{
|
||||
req: req{
|
||||
VER: 5, // socks5
|
||||
CMD: 1, // CONNECT
|
||||
RSV: 0, // RESERVED
|
||||
ATYP: 3, // DOMAINNAME
|
||||
},
|
||||
DST_ADDR: append([]byte{byte(len(c.domain))}, []byte(c.domain)...),
|
||||
DST_PORT: portBuf,
|
||||
}
|
||||
|
||||
if _, err := c.Conn.Write(req.Bytes()); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
{
|
||||
resp := &response{}
|
||||
if err := binary.Read(c.Conn, binary.BigEndian, &(resp.resp)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
switch resp.REP {
|
||||
case 0x00:
|
||||
default:
|
||||
return 0, fmt.Errorf("socks5 handshake fail, return code: %d", resp.REP)
|
||||
}
|
||||
|
||||
switch resp.ATYP {
|
||||
case 0x01: // IPv4
|
||||
resp.DST_ADDR = make([]byte, net.IPv4len)
|
||||
if _, err := io.ReadFull(c.Conn, resp.DST_ADDR); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
case 0x03: // domain name
|
||||
if _, err := io.ReadFull(c.Conn, resp.DST_ADDR[:1]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if _, err := io.ReadFull(c.Conn, resp.DST_ADDR[1:1+int(resp.DST_ADDR[0])]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
case 0x04:
|
||||
resp.DST_ADDR = make([]byte, net.IPv6len)
|
||||
if _, err := io.ReadFull(c.Conn, resp.DST_ADDR); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
resp.DST_PORT = make([]byte, 2)
|
||||
if _, err := io.ReadFull(c.Conn, resp.DST_PORT); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
close(c.init)
|
||||
return c.Conn.Write(b)
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package socks5
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
// https://tools.ietf.org/html/rfc1928
|
||||
|
||||
// 1. client send auth request
|
||||
type authReq struct {
|
||||
VER byte
|
||||
NMETHODS uint8
|
||||
METHODS []byte
|
||||
}
|
||||
|
||||
func (req *authReq) Fulfill(r io.Reader) error {
|
||||
buf := make([]byte, 2)
|
||||
if n, err := io.ReadFull(r, buf); err != nil || n != 2 {
|
||||
return err
|
||||
}
|
||||
|
||||
req.VER = buf[0]
|
||||
req.NMETHODS = buf[1]
|
||||
|
||||
req.METHODS = make([]byte, int(req.NMETHODS))
|
||||
if n, err := io.ReadFull(r, req.METHODS); err != nil || n != len(req.METHODS) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *authReq) IsValid() bool {
|
||||
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
|
||||
type authResp struct {
|
||||
VER byte
|
||||
METHOD byte
|
||||
}
|
||||
|
||||
// 3. client request with target address
|
||||
type reqHead struct {
|
||||
VER byte
|
||||
CMD byte
|
||||
RSV byte
|
||||
ATYP byte
|
||||
}
|
||||
|
||||
func (r *reqHead) IsValid() bool {
|
||||
return r.VER == 5 && r.CMD == 1 && r.RSV == 0
|
||||
}
|
||||
|
||||
// 4. server response with the address that assigned to connect to target address
|
||||
type respHead struct {
|
||||
VER byte
|
||||
REP byte
|
||||
RSV byte
|
||||
ATYP byte
|
||||
BND_ADDR [net.IPv4len]byte
|
||||
BND_PORT uint16
|
||||
}
|
||||
|
||||
type addrType interface {
|
||||
Fulfill(r io.Reader) error
|
||||
Addr() (domain string, port uint16)
|
||||
}
|
||||
|
||||
// ATYP:
|
||||
//
|
||||
// 0x01 -> net.IPv4len
|
||||
// 0x03 -> first byte is length
|
||||
// 0x04 -> net.IPv6len
|
||||
type addrTypeIPv4 struct {
|
||||
DST_ADDR [4]byte
|
||||
DST_PORT uint16
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type addrTypeDomain struct {
|
||||
DST_ADDR_LEN uint8
|
||||
DST_ADDR []byte
|
||||
DST_PORT uint16
|
||||
}
|
||||
|
||||
func (a *addrTypeDomain) Fulfill(r io.Reader) error {
|
||||
buf := make([]byte, 1)
|
||||
if _, err := io.ReadFull(r, buf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.DST_ADDR_LEN = uint8(buf[0])
|
||||
buf = make([]byte, a.DST_ADDR_LEN+2)
|
||||
if _, err := io.ReadFull(r, buf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.DST_ADDR = buf[:int(a.DST_ADDR_LEN)]
|
||||
a.DST_PORT = binary.BigEndian.Uint16(buf[int(a.DST_ADDR_LEN):])
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *addrTypeDomain) Addr() (string, uint16) {
|
||||
return string(a.DST_ADDR[:]), a.DST_PORT
|
||||
}
|
||||
|
||||
type addrTypeIPv6 struct {
|
||||
DST_ADDR [16]byte
|
||||
DST_PORT uint16
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package socks5
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type AddrHead struct {
|
||||
addrType
|
||||
}
|
||||
|
||||
func (h *AddrHead) Network() string { return "tcp" }
|
||||
func (h *AddrHead) String() string {
|
||||
host, port := h.Addr()
|
||||
return net.JoinHostPort(host, strconv.Itoa(int(port)))
|
||||
}
|
||||
|
||||
// Socks5 is a SOCKS5 proxy. It implements the teeconn.Conn interface.
|
||||
// It is used to be a second relay of other proxy tools.
|
||||
// user -> sower -socks5-> third-party proxy -> target
|
||||
type Socks5 struct{}
|
||||
|
||||
func New() *Socks5 {
|
||||
return &Socks5{}
|
||||
}
|
||||
|
||||
const (
|
||||
RepSucceeded = 0x00
|
||||
RepGeneralFailure = 0x01
|
||||
RepConnectionNotAllowed = 0x02
|
||||
)
|
||||
|
||||
var (
|
||||
noAuthResp = authResp{VER: 5, METHOD: 0}
|
||||
)
|
||||
|
||||
func (s *Socks5) Unwrap(conn net.Conn) (net.Addr, error) {
|
||||
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 auth head: %v, err: %s", auth, err)
|
||||
}
|
||||
|
||||
if err := binary.Write(conn, binary.BigEndian, noAuthResp); err != nil {
|
||||
return nil, errors.Wrap(err, "write auth")
|
||||
}
|
||||
}
|
||||
|
||||
var addr addrType
|
||||
{ // head
|
||||
head := new(reqHead)
|
||||
if err := binary.Read(conn, binary.BigEndian, head); err != nil || !head.IsValid() {
|
||||
return nil, errors.Errorf("read head: %v, err: %s", head, err)
|
||||
}
|
||||
switch head.ATYP {
|
||||
case 0x01: // IPv4
|
||||
addr = &addrTypeIPv4{}
|
||||
case 0x03: // domain name
|
||||
addr = &addrTypeDomain{}
|
||||
case 0x04: // IPv6
|
||||
addr = &addrTypeIPv6{}
|
||||
default:
|
||||
return nil, errors.New("invalid ATYP")
|
||||
}
|
||||
|
||||
if err := addr.Fulfill(conn); err != nil {
|
||||
return nil, errors.Wrap(err, "read target")
|
||||
}
|
||||
}
|
||||
|
||||
return &AddrHead{
|
||||
addrType: addr,
|
||||
}, 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
|
||||
METHODS byte
|
||||
}{5, 1, 0}
|
||||
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)
|
||||
}
|
||||
|
||||
resp := &authResp{}
|
||||
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))
|
||||
_ = binary.Write(buf, binary.BigEndian, domainHead)
|
||||
buf.WriteByte(uint8(len(tgtHost)))
|
||||
buf.WriteString(tgtHost)
|
||||
buf.Write([]byte{byte(tgtPort >> 8), byte(tgtPort)})
|
||||
|
||||
if n, err := conn.Write(buf.Bytes()); err != nil || n != len(buf.Bytes()) {
|
||||
return errors.Errorf("n: %d, err: %s", n, err)
|
||||
}
|
||||
|
||||
head := reqHead{}
|
||||
if err := binary.Read(conn, binary.BigEndian, &head); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if head.VER != 5 || head.RSV != 0 {
|
||||
return errors.Errorf("unexpected response head: %+v", head)
|
||||
}
|
||||
if head.CMD != 0 {
|
||||
return errors.Errorf("connect rejected, rep=%d", head.CMD)
|
||||
}
|
||||
|
||||
var addr addrType
|
||||
switch head.ATYP {
|
||||
case 0x01:
|
||||
addr = &addrTypeIPv4{}
|
||||
case 0x03:
|
||||
addr = &addrTypeDomain{}
|
||||
case 0x04:
|
||||
addr = &addrTypeIPv6{}
|
||||
default:
|
||||
return errors.Errorf("invalid response ATYP: %d", head.ATYP)
|
||||
}
|
||||
if err := addr.Fulfill(conn); err != nil {
|
||||
return errors.Wrap(err, "read response address")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package socks5
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type mockConn struct {
|
||||
reader io.Reader
|
||||
writes bytes.Buffer
|
||||
}
|
||||
|
||||
func newMockConn(data []byte) *mockConn {
|
||||
return &mockConn{reader: bytes.NewReader(data)}
|
||||
}
|
||||
|
||||
func (c *mockConn) Read(p []byte) (int, error) { return c.reader.Read(p) }
|
||||
func (c *mockConn) Write(p []byte) (int, error) { return c.writes.Write(p) }
|
||||
func (c *mockConn) Close() error { return nil }
|
||||
func (c *mockConn) LocalAddr() net.Addr { return dummyAddr("local") }
|
||||
func (c *mockConn) RemoteAddr() net.Addr { return dummyAddr("remote") }
|
||||
func (c *mockConn) SetDeadline(time.Time) error { return nil }
|
||||
func (c *mockConn) SetReadDeadline(time.Time) error { return nil }
|
||||
func (c *mockConn) SetWriteDeadline(time.Time) error { return nil }
|
||||
|
||||
type dummyAddr string
|
||||
|
||||
func (a dummyAddr) Network() string { return "tcp" }
|
||||
func (a dummyAddr) String() string { return string(a) }
|
||||
|
||||
func TestUnwrapRejectsZeroMethodsWithoutPanic(t *testing.T) {
|
||||
conn := newMockConn([]byte{0x05, 0x00})
|
||||
if _, err := New().Unwrap(conn); err == nil {
|
||||
t.Fatal("expected error for zero auth methods")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnwrapAcceptsNoAuthWhenNotFirstMethod(t *testing.T) {
|
||||
req := []byte{
|
||||
0x05, 0x02, 0x02, 0x00,
|
||||
0x05, 0x01, 0x00, 0x03,
|
||||
0x0b,
|
||||
}
|
||||
req = append(req, []byte("example.com")...)
|
||||
req = append(req, 0x01, 0xbb)
|
||||
|
||||
addr, err := New().Unwrap(newMockConn(req))
|
||||
if err != nil {
|
||||
t.Fatalf("unwrap failed: %v", err)
|
||||
}
|
||||
|
||||
host, port := addr.(*AddrHead).Addr()
|
||||
if host != "example.com" || port != 443 {
|
||||
t.Fatalf("unexpected addr: %s:%d", host, port)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapRejectsAuthFailure(t *testing.T) {
|
||||
server, client := net.Pipe()
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
buf := make([]byte, 3)
|
||||
if _, err := io.ReadFull(server, buf); err != nil {
|
||||
return
|
||||
}
|
||||
_, _ = server.Write([]byte{0x05, 0xff})
|
||||
}()
|
||||
|
||||
if err := New().Wrap(client, "example.com", 443); err == nil {
|
||||
t.Fatal("expected auth failure")
|
||||
}
|
||||
<-done
|
||||
}
|
||||
|
||||
func TestWrapRejectsConnectFailure(t *testing.T) {
|
||||
server, client := net.Pipe()
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
|
||||
auth := make([]byte, 3)
|
||||
if _, err := io.ReadFull(server, auth); err != nil {
|
||||
return
|
||||
}
|
||||
_, _ = server.Write([]byte{0x05, 0x00})
|
||||
|
||||
if _, err := io.ReadFull(server, make([]byte, 4)); err != nil {
|
||||
return
|
||||
}
|
||||
var nameLen [1]byte
|
||||
if _, err := io.ReadFull(server, nameLen[:]); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err := io.ReadFull(server, make([]byte, int(nameLen[0])+2)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp := bytes.NewBuffer(nil)
|
||||
_ = binary.Write(resp, binary.BigEndian, reqHead{VER: 5, CMD: 5, RSV: 0, ATYP: 1})
|
||||
_, _ = server.Write(resp.Bytes())
|
||||
}()
|
||||
|
||||
if err := New().Wrap(client, "example.com", 443); err == nil {
|
||||
t.Fatal("expected connect failure")
|
||||
}
|
||||
<-done
|
||||
}
|
||||
|
||||
func TestWrapRejectsTooLongHost(t *testing.T) {
|
||||
if err := New().Wrap(newMockConn(nil), string(bytes.Repeat([]byte{'a'}, 256)), 443); err == nil {
|
||||
t.Fatal("expected error for too long host")
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package transport
|
||||
|
||||
// https://tools.ietf.org/html/rfc1928
|
||||
|
||||
type authReq struct {
|
||||
VER byte
|
||||
NMETHODS byte
|
||||
METHODS [1]byte // 1 to 255, fix to no authentication
|
||||
}
|
||||
|
||||
type authResp struct {
|
||||
VER byte
|
||||
METHOD byte
|
||||
}
|
||||
|
||||
type request struct {
|
||||
req
|
||||
DST_ADDR []byte // first byte is length
|
||||
DST_PORT []byte // two bytes
|
||||
}
|
||||
type req struct {
|
||||
VER byte
|
||||
CMD byte
|
||||
RSV byte
|
||||
ATYP byte
|
||||
}
|
||||
|
||||
func (r *request) Bytes() []byte {
|
||||
out := []byte{r.VER, r.CMD, r.RSV, r.ATYP}
|
||||
out = append(out, r.DST_ADDR...)
|
||||
return append(out, r.DST_PORT...)
|
||||
}
|
||||
|
||||
type response struct {
|
||||
resp
|
||||
DST_ADDR []byte // first byte is length
|
||||
DST_PORT []byte // two bytes
|
||||
}
|
||||
type resp struct {
|
||||
VER byte
|
||||
REP byte
|
||||
RSV byte
|
||||
ATYP byte
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package sower
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// https://en.wikipedia.org/wiki/Domain_Name_System
|
||||
const maxDomainLength = 253
|
||||
|
||||
var headSize = binary.Size(new(Head))
|
||||
|
||||
// action(>=0x80) + checksum + port + target + data
|
||||
// data(HTTP, first byte < 0x7F)
|
||||
type Head struct {
|
||||
Cmd byte
|
||||
Checksum uint64
|
||||
Port uint16
|
||||
TgtAddr [maxDomainLength]byte
|
||||
}
|
||||
|
||||
func (h *Head) Network() string { return "tcp" }
|
||||
func (h *Head) String() string {
|
||||
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)))
|
||||
}
|
||||
|
||||
type Sower struct {
|
||||
password []byte
|
||||
}
|
||||
|
||||
func New(password string) *Sower {
|
||||
return &Sower{
|
||||
password: []byte(password),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Sower) Unwrap(conn net.Conn) (net.Addr, error) {
|
||||
buf := make([]byte, headSize)
|
||||
if n, err := io.ReadFull(conn, buf); err != nil || n != headSize {
|
||||
return nil, errors.Errorf("n: %d, err: %s", n, err)
|
||||
}
|
||||
|
||||
h := &Head{}
|
||||
_ = binary.Read(bytes.NewReader(buf), binary.BigEndian, h)
|
||||
switch h.Cmd {
|
||||
case 0x80:
|
||||
default:
|
||||
return nil, errors.Errorf("invalid command: %d", h.Cmd)
|
||||
}
|
||||
|
||||
if h.Checksum != sumChecksum(h.TgtAddr, s.password) {
|
||||
return nil, errors.New("auth fail")
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
return binary.Write(conn, binary.BigEndian, &Head{
|
||||
Cmd: 0x80,
|
||||
Checksum: sumChecksum(tgtAddr, s.password),
|
||||
Port: tgtPort,
|
||||
TgtAddr: tgtAddr,
|
||||
})
|
||||
}
|
||||
|
||||
func sumChecksum(target [maxDomainLength]byte, password []byte) uint64 {
|
||||
checksum := md5.Sum(append(target[:], password...))
|
||||
checksumVal, _ := binary.Uvarint(checksum[:])
|
||||
return checksumVal
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
type Transport interface {
|
||||
Unwrap(conn net.Conn) (net.Addr, error)
|
||||
Wrap(conn net.Conn, tgtHost string, tgtPort uint16) error
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/sower-proxy/conns/reread"
|
||||
"github.com/sower-proxy/sower/transport/sower"
|
||||
"github.com/sower-proxy/sower/transport/trojan"
|
||||
)
|
||||
|
||||
func testPipe(tran Transport) (net.Addr, error) {
|
||||
r, w := net.Pipe()
|
||||
defer r.Close()
|
||||
|
||||
go func(w net.Conn) {
|
||||
defer w.Close()
|
||||
tran.Wrap(w, "sower", 443)
|
||||
}(w)
|
||||
|
||||
return tran.Unwrap(reread.New(r))
|
||||
}
|
||||
|
||||
func Test_Transports(t *testing.T) {
|
||||
if addr, err := testPipe(newSower()); err != nil || strings.TrimSpace(addr.String()) != "sower:443" {
|
||||
t.Errorf("test sower, unexpected address: %s, err: %s", addr, err)
|
||||
}
|
||||
|
||||
if addr, err := testPipe(newTrojan()); err != nil || strings.TrimSpace(addr.String()) != "sower:443" {
|
||||
t.Errorf("test trojan, unexpected address: %s, err: %s", addr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func newSower() *sower.Sower {
|
||||
return sower.New("123")
|
||||
}
|
||||
|
||||
func newTrojan() *trojan.Trojan {
|
||||
return trojan.New("123")
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package trojan
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// +-----------------------+---------+----------------+---------+----------+
|
||||
// | hex(SHA224(password)) | CRLF | Trojan Request | CRLF | Payload |
|
||||
// +-----------------------+---------+----------------+---------+----------+
|
||||
// | 56 | X'0D0A' | Variable | X'0D0A' | Variable |
|
||||
// +-----------------------+---------+----------------+---------+----------+
|
||||
// +-----+------+----------+----------+
|
||||
// | CMD | ATYP | DST.ADDR | DST.PORT |
|
||||
// +-----+------+----------+----------+
|
||||
// | 1 | 1 | Variable | 2 |
|
||||
// +-----+------+----------+----------+
|
||||
// o CMD
|
||||
// o CONNECT X'01'
|
||||
// o UDP :X'03'
|
||||
// o ATYP
|
||||
// o IP V4 : X'01'
|
||||
// o domain: X'03'
|
||||
// o IP V6 : X'04'
|
||||
|
||||
const headLen = 56 + 2 + 1 + 1
|
||||
|
||||
type staticHead struct {
|
||||
Passwd [56]byte
|
||||
CRLF [2]byte
|
||||
CMD uint8
|
||||
ATYP uint8
|
||||
}
|
||||
|
||||
type ipv4Addr struct {
|
||||
ADDR [4]byte
|
||||
PORT uint16
|
||||
CRLF [2]byte
|
||||
}
|
||||
|
||||
func (*ipv4Addr) Network() string { return "tcp" }
|
||||
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
|
||||
CRLF [2]byte
|
||||
}
|
||||
|
||||
func (*ipv6Addr) Network() string { return "tcp" }
|
||||
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
|
||||
CRLF [2]byte
|
||||
}
|
||||
|
||||
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 := 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 := 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
|
||||
|
||||
headIPv4 []byte
|
||||
headIPv6 []byte
|
||||
headDomain []byte
|
||||
}
|
||||
|
||||
func New(password string) *Trojan {
|
||||
t := &Trojan{
|
||||
headPasswd: make([]byte, 56),
|
||||
}
|
||||
passSum := sha256.Sum224([]byte(password))
|
||||
hex.Encode(t.headPasswd, passSum[:])
|
||||
|
||||
t.headIPv4 = append(t.headPasswd, 0x0D, 0x0A, 0x01, 0x01)
|
||||
t.headIPv6 = append(t.headPasswd, 0x0D, 0x0A, 0x01, 0x04)
|
||||
t.headDomain = append(t.headPasswd, 0x0D, 0x0A, 0x01, 0x03)
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *Trojan) Unwrap(conn net.Conn) (net.Addr, error) {
|
||||
buf := make([]byte, headLen)
|
||||
if n, err := io.ReadFull(conn, buf); err != nil || n != headLen {
|
||||
return nil, errors.Errorf("n: %d, err: %s", n, err)
|
||||
}
|
||||
|
||||
head := &staticHead{}
|
||||
if err := binary.Read(bytes.NewBuffer(buf), binary.BigEndian, head); err != nil {
|
||||
return nil, errors.Wrap(err, "read head")
|
||||
}
|
||||
|
||||
if !bytes.Equal(head.Passwd[:], []byte(t.headPasswd)) {
|
||||
return nil, errors.New("auth fail")
|
||||
}
|
||||
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
|
||||
addr := &ipv4Addr{}
|
||||
err := binary.Read(conn, binary.BigEndian, 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
|
||||
addr := &ipv6Addr{}
|
||||
err := binary.Read(conn, binary.BigEndian, 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)
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Trojan) Wrap(conn net.Conn, tgtHost string, tgtPort uint16) error {
|
||||
buf := bytes.NewBuffer(make([]byte, 0, headLen+1+len(tgtHost)+4))
|
||||
ip := net.ParseIP(tgtHost)
|
||||
switch {
|
||||
case len(ip.To4()) != 0:
|
||||
buf.Write(t.headIPv4)
|
||||
buf.Write([]byte(ip.To4()))
|
||||
|
||||
case len(ip) != 0:
|
||||
buf.Write(t.headIPv6)
|
||||
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)
|
||||
}
|
||||
|
||||
buf.Write([]byte{byte(tgtPort >> 8), byte(tgtPort), 0x0D, 0x0A})
|
||||
|
||||
if n, err := conn.Write(buf.Bytes()); err != nil || n != len(buf.Bytes()) {
|
||||
return errors.Errorf("n: %d, err: %s", n, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/wweir/sower/dhcp"
|
||||
"github.com/wweir/utils/log"
|
||||
)
|
||||
|
||||
var (
|
||||
persistDNS string
|
||||
dnsAddr string
|
||||
resolver = &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return (&net.Dialer{}).DialContext(ctx, network, dnsAddr)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func SetDNS(err error, dnsIP string) {
|
||||
if dnsIP != "" {
|
||||
persistDNS = dnsIP
|
||||
dnsAddr = net.JoinHostPort(dnsIP, "53")
|
||||
return
|
||||
} else if persistDNS != "" {
|
||||
return
|
||||
}
|
||||
|
||||
if e, ok := err.(*net.DNSError); !ok /*nil*/ || !e.IsNotFound {
|
||||
if dnsIP, err = dhcp.GetDefaultDNSServer(); err != nil {
|
||||
dnsIP, err = dhcp.GetDefaultDNSServer() // retry
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorw("get dns via dhcp", "err", err, "current_dns", dnsAddr)
|
||||
} else {
|
||||
dnsAddr = net.JoinHostPort(dnsIP, "53")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dial dial targetAddr with possiable proxy address
|
||||
func Dial(targetAddr string, dialAddr func(domain string) (proxyAddr string, password []byte)) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(targetAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
address, password := dialAddr(host)
|
||||
if address == "" {
|
||||
ips, err := resolver.LookupIPAddr(context.Background(), host)
|
||||
if err != nil { //retry
|
||||
ips, err = resolver.LookupIPAddr(context.Background(), host)
|
||||
}
|
||||
if err != nil {
|
||||
SetDNS(err, "")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return net.Dial("tcp", net.JoinHostPort(ips[0].String(), port))
|
||||
}
|
||||
|
||||
p, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if addr, ok := IsSocks5Schema(address); ok {
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if conn, err = ToSocks5(conn, host, uint16(p)); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
conn, err := tls.DialWithDialer(&net.Dialer{Resolver: resolver},
|
||||
"tcp", net.JoinHostPort(address, "443"), &tls.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ToProxyConn(conn, host, uint16(p), password)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ReverseSecSlice struct {
|
||||
sort.StringSlice
|
||||
}
|
||||
|
||||
func NewReverseSecSlice(a []string) *ReverseSecSlice {
|
||||
return &ReverseSecSlice{sort.StringSlice(a)}
|
||||
}
|
||||
|
||||
func (p *ReverseSecSlice) Sort() *ReverseSecSlice {
|
||||
sort.Sort(p)
|
||||
return p
|
||||
}
|
||||
func (p *ReverseSecSlice) Uniq() []string {
|
||||
olds := []string(p.StringSlice)
|
||||
|
||||
last := ""
|
||||
strs := make([]string, 0, len(olds))
|
||||
for _, str := range olds {
|
||||
if str != last {
|
||||
strs = append(strs, str)
|
||||
}
|
||||
last = str
|
||||
}
|
||||
return strs
|
||||
}
|
||||
|
||||
func (p *ReverseSecSlice) Less(i, j int) bool {
|
||||
secsI := strings.Split(p.StringSlice[i], ".")
|
||||
secsJ := strings.Split(p.StringSlice[j], ".")
|
||||
|
||||
lenI := len(secsI) - 1
|
||||
lenJ := len(secsJ) - 1
|
||||
length := lenI
|
||||
if lenI > lenJ {
|
||||
length = lenJ
|
||||
}
|
||||
|
||||
for idx := 0; idx <= length; idx++ {
|
||||
if secsI[lenI-idx] == secsJ[lenJ-idx] {
|
||||
continue
|
||||
}
|
||||
return secsI[lenI-idx] < secsJ[lenJ-idx]
|
||||
}
|
||||
return lenI < lenJ
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReverseSecSlice_Less(t *testing.T) {
|
||||
type fields struct {
|
||||
StringSlice sort.StringSlice
|
||||
}
|
||||
type args struct {
|
||||
i int
|
||||
j int
|
||||
}
|
||||
slice := fields{sort.StringSlice([]string{
|
||||
"a.b.c",
|
||||
"a.b.c",
|
||||
"d.b.c",
|
||||
"d.a.b.c",
|
||||
"",
|
||||
"",
|
||||
})}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want bool
|
||||
}{{
|
||||
name: "equal",
|
||||
fields: slice,
|
||||
args: args{0, 1},
|
||||
want: false,
|
||||
}, {
|
||||
name: "less",
|
||||
fields: slice,
|
||||
args: args{0, 2},
|
||||
want: true,
|
||||
}, {
|
||||
name: "length",
|
||||
fields: slice,
|
||||
args: args{0, 3},
|
||||
want: true,
|
||||
}, {
|
||||
name: "single_empty",
|
||||
fields: slice,
|
||||
args: args{0, 4},
|
||||
want: false,
|
||||
}, {
|
||||
name: "all_empty",
|
||||
fields: slice,
|
||||
args: args{4, 5},
|
||||
want: false,
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := &ReverseSecSlice{
|
||||
StringSlice: tt.fields.StringSlice,
|
||||
}
|
||||
if got := p.Less(tt.args.i, tt.args.j); got != tt.want {
|
||||
t.Errorf("ReverseSecSlice.Less() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Node struct {
|
||||
node
|
||||
sep string
|
||||
*sync.RWMutex
|
||||
}
|
||||
type node struct {
|
||||
node map[string]*node
|
||||
}
|
||||
|
||||
func NewNodeFromRules(rules ...string) *Node {
|
||||
n := &Node{node{node: map[string]*node{}}, ".", &sync.RWMutex{}}
|
||||
for i := range rules {
|
||||
n.Add(rules[i])
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *Node) String() string {
|
||||
n.RLock()
|
||||
defer n.RUnlock()
|
||||
return n.string("", " ")
|
||||
}
|
||||
func (n *node) string(prefix, indent string) (out string) {
|
||||
for key, val := range n.node {
|
||||
out += prefix + key + "\n" + val.string(prefix+indent, indent)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (n *Node) trim(item string) string {
|
||||
return strings.TrimSuffix(item, n.sep)
|
||||
}
|
||||
|
||||
func (n *Node) Add(item string) {
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
n.add(strings.Split(n.trim(item), n.sep))
|
||||
}
|
||||
func (n *node) add(secs []string) {
|
||||
length := len(secs)
|
||||
switch length {
|
||||
case 0:
|
||||
return
|
||||
case 1:
|
||||
n.node[secs[length-1]] = &node{node: map[string]*node{"": &node{}}}
|
||||
default:
|
||||
sec := secs[length-1]
|
||||
if sec == "**" {
|
||||
sec = "*"
|
||||
}
|
||||
|
||||
subNode, ok := n.node[sec]
|
||||
if !ok {
|
||||
subNode = &node{node: map[string]*node{}}
|
||||
n.node[sec] = subNode
|
||||
}
|
||||
subNode.add(secs[:length-1])
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Node) Match(item string) bool {
|
||||
if n == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
n.RLock()
|
||||
defer n.RUnlock()
|
||||
return n.matchSecs(strings.Split(n.trim(item), n.sep), false)
|
||||
}
|
||||
|
||||
func (n *node) matchSecs(secs []string, fuzzNode bool) bool {
|
||||
length := len(secs)
|
||||
if length == 0 {
|
||||
if _, ok := n.node[""]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := n.node["**"]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := n.node["*"]; ok {
|
||||
return !fuzzNode
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if n, ok := n.node[secs[length-1]]; ok {
|
||||
if n.matchSecs(secs[:length-1], false) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if n, ok := n.node["*"]; ok {
|
||||
if n.matchSecs(secs[:length-1], true) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if _, ok := n.node["**"]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package util_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/wweir/sower/util"
|
||||
)
|
||||
|
||||
func TestNode_Match(t *testing.T) {
|
||||
type test struct {
|
||||
arg string
|
||||
want bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
node *util.Node
|
||||
tests []test
|
||||
}{{
|
||||
"simple",
|
||||
util.NewNodeFromRules("a.wweir.cc", "b.wweir.cc"),
|
||||
[]test{
|
||||
{"a.wweir.cc", true},
|
||||
{"b.wweir.cc", true},
|
||||
},
|
||||
}, {
|
||||
"parent",
|
||||
util.NewNodeFromRules("wweir.cc", "a.wweir.cc"),
|
||||
[]test{
|
||||
{"wweir.cc", true},
|
||||
{"a.wweir.cc", true},
|
||||
{"b.wweir.cc", false},
|
||||
},
|
||||
}, {
|
||||
"fuzz1",
|
||||
util.NewNodeFromRules("wweir.cc", "a.wweir.cc", "*.wweir.cc"),
|
||||
[]test{
|
||||
{"wweir.cc", true},
|
||||
{"a.wweir.cc", true},
|
||||
{"b.wweir.cc", true},
|
||||
{"a.b.wweir.cc", false},
|
||||
},
|
||||
}, {
|
||||
"fuzz2",
|
||||
util.NewNodeFromRules("a.*.cc", "c.wweir.*"),
|
||||
[]test{
|
||||
{"wweir.cc", false},
|
||||
{"a.wweir.cc", true},
|
||||
{"b.wweir.cc", false},
|
||||
{"c.wweir.cc", true},
|
||||
},
|
||||
}, {
|
||||
"fuzz3",
|
||||
util.NewNodeFromRules("*.*.cc", "iamp.*.*"),
|
||||
[]test{
|
||||
{"wweir.cc", false},
|
||||
{"a.wweir.cc", true},
|
||||
{"b.wweir.cc", true},
|
||||
{"iamp.wweir.cc", true},
|
||||
},
|
||||
}, {
|
||||
"fuzz4",
|
||||
util.NewNodeFromRules("**.cc", "a.**.com", "**.wweir.*"),
|
||||
[]test{
|
||||
{"wweir.cc", true},
|
||||
{"a.wweir.cc", true},
|
||||
{"a.b.wweir.cc", true},
|
||||
{"a.fuzz.com", true},
|
||||
{"b.fuzz.com", false},
|
||||
{"www.wweir.com", true},
|
||||
},
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
for _, test := range tt.tests {
|
||||
if got := tt.node.Match(test.arg); got != test.want {
|
||||
t.Errorf("Node.Match(%s) = %v, want %v", test.arg, got, test.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
type TeeConn struct {
|
||||
net.Conn
|
||||
buf []byte
|
||||
offset int
|
||||
stop bool // read
|
||||
EnableWrite bool
|
||||
}
|
||||
|
||||
func (t *TeeConn) Reread() {
|
||||
t.offset = 0
|
||||
}
|
||||
func (t *TeeConn) Reset() {
|
||||
t.buf = []byte{}
|
||||
t.offset = 0
|
||||
}
|
||||
func (t *TeeConn) Stop() {
|
||||
t.offset = 0
|
||||
t.stop = true
|
||||
}
|
||||
|
||||
func (t *TeeConn) Read(b []byte) (n int, err error) {
|
||||
length := len(t.buf) - t.offset
|
||||
if length > 0 {
|
||||
n = copy(b, t.buf[t.offset:])
|
||||
t.offset += n
|
||||
return
|
||||
}
|
||||
|
||||
n, err = t.Conn.Read(b)
|
||||
if !t.stop {
|
||||
t.buf = append(t.buf, b[:n]...)
|
||||
t.offset += n
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (t *TeeConn) Write(b []byte) (n int, err error) {
|
||||
if t.stop || t.EnableWrite {
|
||||
return t.Conn.Write(b)
|
||||
}
|
||||
|
||||
return 0, io.EOF
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package util
|
||||
|
||||
import "net"
|
||||
|
||||
func WithDefaultPort(addr string, port string) (address, host string) {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return net.JoinHostPort(addr, port), addr
|
||||
}
|
||||
return addr, host
|
||||
}
|
||||
Reference in New Issue
Block a user