Launch
This commit is contained in:
413
README.md
Normal file
413
README.md
Normal file
@@ -0,0 +1,413 @@
|
||||
# SocksRevive VOID PC
|
||||
|
||||
SocksRevive VOID PC is a native desktop tunnel client for Windows and Linux. It provides offline profile management, SSH-based tunnel modes, embedded TUN routing, optional IPv6, UDPGW support, DNSTT, and Xray integration.
|
||||
|
||||
The application uses a native desktop interface. Profiles are created, imported, and exported from the app as `.srpc` files.
|
||||
|
||||
## Features
|
||||
|
||||
| Feature | Description |
|
||||
|---|---|
|
||||
| Direct SSH | Creates a local SOCKS5 server and forwards traffic through SSH. |
|
||||
| Payload SSH | Connects through an HTTP proxy/payload before the SSH handshake. |
|
||||
| SSL/TLS SSH | Wraps the SSH tunnel in TLS/SNI. |
|
||||
| Payload + SSL | Uses TLS/SNI, payload injection, and SSH together. |
|
||||
| DNSTT + SSH | Uses the embedded DNSTT client and then connects SSH through the local DNSTT endpoint. |
|
||||
| Xray Core | Starts an external Xray executable and routes traffic through its local SOCKS inbound. |
|
||||
| TUN mode | Routes system traffic through the tunnel using embedded tun2socks. |
|
||||
| UDPGW | Enables UDP traffic for SSH-based modes when a server-side UDPGW service is available. |
|
||||
| IPv6 | Optional IPv6 routing and IPv6 leak protection. |
|
||||
| Reconnect | Automatically reconnects after tunnel loss when enabled in the profile. |
|
||||
| Proxy rotation | Tries multiple proxy hosts until one completes the full tunnel handshake. |
|
||||
|
||||
## Project folders
|
||||
|
||||
```text
|
||||
profiles/ Local offline .srpc profiles
|
||||
configs/ Xray configuration files
|
||||
tools/xray/ Xray executable location
|
||||
tools/wintun/ Wintun DLL source location before embedding
|
||||
dist/ Build output
|
||||
logs/ Runtime and crash logs
|
||||
```
|
||||
|
||||
## Profile format
|
||||
|
||||
Profiles use the `.srpc` extension. They are managed by the app UI and stored locally in the `profiles/` folder.
|
||||
|
||||
Xray is the only mode that uses a JSON file directly, because Xray Core requires JSON configuration. The default Xray configuration path is:
|
||||
|
||||
```text
|
||||
configs/xray.json
|
||||
```
|
||||
|
||||
## Windows build requirements
|
||||
|
||||
Install:
|
||||
|
||||
- Go 1.22 or newer
|
||||
- MSYS2 UCRT64 GCC
|
||||
- MSYS2 binutils, for `windres.exe`
|
||||
|
||||
Automatic setup:
|
||||
|
||||
```powershell
|
||||
cd SocksRevivePC
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\install_windows_compiler_msys2.ps1
|
||||
```
|
||||
|
||||
After setup finishes, open a new PowerShell window and build:
|
||||
|
||||
```powershell
|
||||
cd SocksRevivePC
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\build_windows.ps1
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
.\dist\SocksRevivePC.exe
|
||||
```
|
||||
|
||||
The Windows executable includes a UAC manifest, so Windows should ask for Administrator permission when the app starts. Administrator permission is required for Wintun, DNS changes, TUN addresses, and system routes.
|
||||
|
||||
The normal Windows build is GUI-only. Helper processes such as route configuration, DNS configuration, Xray, and legacy helper binaries are started without visible console windows.
|
||||
|
||||
Optional debug build:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\build_windows.ps1 -Debug
|
||||
```
|
||||
|
||||
Debug builds may show a console and should only be used for troubleshooting.
|
||||
|
||||
## Linux build requirements
|
||||
|
||||
Install Go, GCC, and the desktop dependencies required by Fyne. On Debian or Ubuntu:
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y golang gcc libgl1-mesa-dev xorg-dev
|
||||
```
|
||||
|
||||
Build and run:
|
||||
|
||||
```bash
|
||||
cd SocksRevivePC
|
||||
chmod +x scripts/build_linux.sh
|
||||
./scripts/build_linux.sh
|
||||
./dist/socksrevivepc
|
||||
```
|
||||
|
||||
For TUN mode on Linux, run the app with root privileges:
|
||||
|
||||
```bash
|
||||
sudo ./dist/socksrevivepc
|
||||
```
|
||||
|
||||
## Wintun setup for Windows TUN mode
|
||||
|
||||
Windows TUN mode requires the official signed `wintun.dll`.
|
||||
|
||||
For 64-bit Windows, place the DLL here before building:
|
||||
|
||||
```text
|
||||
tools/wintun/amd64/wintun.dll
|
||||
```
|
||||
|
||||
Embed it into the final executable:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\embed_wintun_from_tools.ps1
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\build_windows.ps1
|
||||
```
|
||||
|
||||
After embedding, the app extracts the DLL automatically at runtime. Users do not need to manually place `wintun.dll` beside the executable.
|
||||
|
||||
Fallback lookup locations are also supported:
|
||||
|
||||
```text
|
||||
Same folder as SocksRevivePC.exe
|
||||
tools/wintun/amd64/wintun.dll
|
||||
PATH
|
||||
```
|
||||
|
||||
Use only the official signed Wintun DLL from the WireGuard/Wintun distribution.
|
||||
|
||||
## TUN settings
|
||||
|
||||
Recommended Windows values:
|
||||
|
||||
```text
|
||||
Device: wintun
|
||||
Interface name: wintun
|
||||
MTU: 1500
|
||||
CIDR: 198.18.0.1/15
|
||||
Gateway: 198.18.0.1
|
||||
Route all traffic: enabled
|
||||
```
|
||||
|
||||
When TUN mode starts, the app:
|
||||
|
||||
1. Starts the selected tunnel.
|
||||
2. Waits for the local SOCKS port.
|
||||
3. Starts the embedded tun2socks engine.
|
||||
4. Configures the TUN adapter.
|
||||
5. Adds system routes.
|
||||
6. Adds bypass routes for the active server or active proxy.
|
||||
|
||||
For route-all mode on Windows, the app installs split default routes through Wintun:
|
||||
|
||||
```text
|
||||
0.0.0.0/1
|
||||
128.0.0.0/1
|
||||
```
|
||||
|
||||
Only the active proxy from a rotation list is bypassed. The full proxy list is not added to the route table.
|
||||
|
||||
## IPv6 settings
|
||||
|
||||
IPv6 is optional per profile.
|
||||
|
||||
Recommended when the server supports IPv6:
|
||||
|
||||
```text
|
||||
Enable IPv6 through tunnel: enabled
|
||||
IPv6 CIDR: fd00:534f:434b::1/64
|
||||
IPv6 DNS: 2606:4700:4700::1111, 2001:4860:4860::8888
|
||||
Block IPv6 leaks when IPv6 tunnel is off: enabled
|
||||
```
|
||||
|
||||
Recommended when the server does not support IPv6:
|
||||
|
||||
```text
|
||||
Enable IPv6 through tunnel: disabled
|
||||
Block IPv6 leaks when IPv6 tunnel is off: enabled
|
||||
```
|
||||
|
||||
When IPv6 routing or IPv6 leak protection is enabled, the app uses these IPv6 split routes:
|
||||
|
||||
```text
|
||||
::/1
|
||||
8000::/1
|
||||
```
|
||||
|
||||
## UDPGW settings
|
||||
|
||||
UDPGW allows real UDP traffic through SSH-based modes.
|
||||
|
||||
Supported modes:
|
||||
|
||||
```text
|
||||
Direct SSH
|
||||
Payload SSH
|
||||
SSL/TLS SSH
|
||||
Payload + SSL
|
||||
DNSTT + SSH
|
||||
```
|
||||
|
||||
Recommended server-side listener:
|
||||
|
||||
```text
|
||||
127.0.0.1:7400
|
||||
```
|
||||
|
||||
Recommended client values in the UDPGW tab:
|
||||
|
||||
```text
|
||||
Enabled: on
|
||||
Protocol: badvpn
|
||||
Remote UDPGW host: 127.0.0.1
|
||||
Remote UDPGW port: 7400
|
||||
```
|
||||
|
||||
`127.0.0.1` is resolved from the server side because the UDPGW connection is made through SSH.
|
||||
|
||||
Protocol options:
|
||||
|
||||
```text
|
||||
badvpn Android-compatible BadVPN UDPGW framing with IPv4 and IPv6 target support
|
||||
legacy Older IPv4-only frame format
|
||||
```
|
||||
|
||||
When UDPGW is disabled, SSH modes keep a DNS-only fallback: UDP port 53 is converted to DNS-over-TCP through SSH, while other UDP traffic is dropped.
|
||||
|
||||
## DNSTT setup
|
||||
|
||||
DNSTT mode uses the embedded DNSTT client. Normal DNSTT profiles do not require an external `dnstt-client.exe`.
|
||||
|
||||
Create or edit a profile, choose `DNSTT + SSH`, and set:
|
||||
|
||||
```text
|
||||
Embedded engine: enabled
|
||||
Resolver type: doh, dot, or udp
|
||||
Resolver: https://cloudflare-dns.com/dns-query, 1.1.1.1:853, or 1.1.1.1:53
|
||||
Tunnel domain: DNSTT tunnel domain
|
||||
Server public key: DNSTT server public key in hex
|
||||
Local SSH host: 127.0.0.1
|
||||
Local SSH port: 2222
|
||||
```
|
||||
|
||||
Then fill the SSH tab with the SSH account behind DNSTT.
|
||||
|
||||
External DNSTT executable fields are kept only for compatibility with older deployments.
|
||||
|
||||
## Xray setup
|
||||
|
||||
Place Xray here:
|
||||
|
||||
```text
|
||||
tools/xray/xray.exe # Windows
|
||||
tools/xray/xray # Linux/macOS
|
||||
```
|
||||
|
||||
Edit the Xray configuration file:
|
||||
|
||||
```text
|
||||
configs/xray.json
|
||||
```
|
||||
|
||||
The app expects Xray to expose a local SOCKS inbound. Default:
|
||||
|
||||
```text
|
||||
127.0.0.1:10808
|
||||
```
|
||||
|
||||
The SOCKS host and port in the UI must match the inbound configured in `configs/xray.json`.
|
||||
|
||||
## Payload syntax
|
||||
|
||||
Supported placeholders:
|
||||
|
||||
```text
|
||||
[host]
|
||||
[port]
|
||||
[crlf]
|
||||
[lf]
|
||||
[cr]
|
||||
[split]
|
||||
[instant_split]
|
||||
[delay=250]
|
||||
[rotate=a.com;b.com;c.com]
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
CONNECT [host]:[port] HTTP/1.1[crlf]Host: [host][crlf]User-Agent: SocksRevivePC[crlf][crlf]
|
||||
```
|
||||
|
||||
Payload modes read and log multiple immediate HTTP responses. This supports proxy chains that return more than one status before the tunnel is ready, such as:
|
||||
|
||||
```text
|
||||
HTTP/1.1 403 Forbidden
|
||||
HTTP/1.1 101 Switching Protocols
|
||||
SSH-2.0-...
|
||||
```
|
||||
|
||||
HTTP status lines are shown in the Logs tab as `Proxy Status` entries.
|
||||
|
||||
## Proxy rotation
|
||||
|
||||
In Payload SSH mode, the Proxy host field supports multiple entries. Separate entries with `#`, comma, semicolon, or new lines.
|
||||
|
||||
Example with a shared port:
|
||||
|
||||
```text
|
||||
52.85.78.104#52.85.78.91#52.85.78.22#52.85.78.28
|
||||
```
|
||||
|
||||
Set the Proxy port field to the shared port, such as:
|
||||
|
||||
```text
|
||||
80
|
||||
```
|
||||
|
||||
Entries may also include individual ports:
|
||||
|
||||
```text
|
||||
52.85.78.104:80#52.85.78.91:8080#proxy.example.com:443
|
||||
```
|
||||
|
||||
The app tries each proxy until the full sequence succeeds:
|
||||
|
||||
```text
|
||||
TCP connect
|
||||
Payload response
|
||||
SSH handshake
|
||||
```
|
||||
|
||||
The `[rotate=...]` payload placeholder supports the same separators:
|
||||
|
||||
```text
|
||||
[rotate=host1#host2#host3]
|
||||
```
|
||||
|
||||
## Auto reconnect
|
||||
|
||||
Reconnect options are in the Main tab:
|
||||
|
||||
```text
|
||||
Reconnect: on/off
|
||||
Reconnect delay seconds: default 3
|
||||
Reconnect max retries: 0 means unlimited
|
||||
Reconnect check seconds: default 10
|
||||
```
|
||||
|
||||
When reconnect is enabled, the app checks the tunnel periodically. If the tunnel is lost, it:
|
||||
|
||||
1. Stops the active tunnel.
|
||||
2. Removes routes.
|
||||
3. Stops tun2socks and releases the TUN adapter.
|
||||
4. Waits for the configured delay.
|
||||
5. Starts the profile again.
|
||||
|
||||
Disconnect and Cancel stop the reconnect loop for the current session.
|
||||
|
||||
## Logs and diagnostics
|
||||
|
||||
Runtime logs are written to:
|
||||
|
||||
```text
|
||||
logs/runtime.log
|
||||
logs/crash.log
|
||||
```
|
||||
|
||||
Windows TUN diagnostics:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\diagnose_windows_tun.ps1
|
||||
```
|
||||
|
||||
Use this when the UI says connected but system traffic is not using the tunnel. The expected Windows route-all entries are:
|
||||
|
||||
```text
|
||||
0.0.0.0/1 through Wintun
|
||||
128.0.0.0/1 through Wintun
|
||||
```
|
||||
|
||||
When IPv6 routing or IPv6 leak protection is enabled, expected IPv6 routes are:
|
||||
|
||||
```text
|
||||
::/1 through Wintun
|
||||
8000::/1 through Wintun
|
||||
```
|
||||
|
||||
## Distribution notes
|
||||
|
||||
For normal Windows users, distribute:
|
||||
|
||||
```text
|
||||
dist/SocksRevivePC.exe
|
||||
```
|
||||
|
||||
Do not distribute debug builds unless support logs are needed.
|
||||
|
||||
If Xray mode is required, include the Xray executable and configuration:
|
||||
|
||||
```text
|
||||
tools/xray/xray.exe
|
||||
configs/xray.json
|
||||
```
|
||||
|
||||
If Wintun is not embedded, include the official signed DLL beside the EXE or in the supported tools folder.
|
||||
BIN
cmd/socksrevivepc/admin_windows.syso
Normal file
BIN
cmd/socksrevivepc/admin_windows.syso
Normal file
Binary file not shown.
31
cmd/socksrevivepc/main.go
Normal file
31
cmd/socksrevivepc/main.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
|
||||
"socksrevivepc/internal/app"
|
||||
"socksrevivepc/internal/crash"
|
||||
"socksrevivepc/internal/nativeui"
|
||||
)
|
||||
|
||||
func main() {
|
||||
debug.SetTraceback("all")
|
||||
root := "."
|
||||
if wd, err := os.Getwd(); err == nil && wd != "" {
|
||||
root = wd
|
||||
}
|
||||
closeLog := crash.AttachLog(root)
|
||||
defer closeLog()
|
||||
defer crash.Recover(root)
|
||||
|
||||
application, err := app.New()
|
||||
if err != nil {
|
||||
crash.Write(root, "init failed", err)
|
||||
log.Printf("init failed: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("app root: %s", application.Root)
|
||||
nativeui.Run(application)
|
||||
}
|
||||
25
cmd/socksrevivepc/socksrevivepc.exe.manifest
Normal file
25
cmd/socksrevivepc/socksrevivepc.exe.manifest
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<assemblyIdentity
|
||||
version="1.0.0.0"
|
||||
processorArchitecture="amd64"
|
||||
name="SocksRevivePC"
|
||||
type="win32" />
|
||||
<description>SocksRevive PC</description>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
|
||||
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
|
||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
3
cmd/socksrevivepc/socksrevivepc_windows.rc
Normal file
3
cmd/socksrevivepc/socksrevivepc_windows.rc
Normal file
@@ -0,0 +1,3 @@
|
||||
// Windows application manifest resource.
|
||||
// RT_MANIFEST is resource type 24.
|
||||
1 24 "cmd/socksrevivepc/socksrevivepc.exe.manifest"
|
||||
33
configs/xray.json
Normal file
33
configs/xray.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"log": { "loglevel": "warning" },
|
||||
"inbounds": [
|
||||
{
|
||||
"tag": "socks-in",
|
||||
"port": 10808,
|
||||
"listen": "127.0.0.1",
|
||||
"protocol": "socks",
|
||||
"settings": { "udp": true }
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"tag": "proxy",
|
||||
"protocol": "vless",
|
||||
"settings": {
|
||||
"vnext": [
|
||||
{
|
||||
"address": "CHANGE_ME_HOST",
|
||||
"port": 443,
|
||||
"users": [ { "id": "CHANGE_ME_UUID", "encryption": "none" } ]
|
||||
}
|
||||
]
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "tcp",
|
||||
"security": "tls",
|
||||
"tlsSettings": { "serverName": "CHANGE_ME_SNI" }
|
||||
}
|
||||
},
|
||||
{ "tag": "direct", "protocol": "freedom" }
|
||||
]
|
||||
}
|
||||
74
go.mod
Normal file
74
go.mod
Normal file
@@ -0,0 +1,74 @@
|
||||
module socksrevivepc
|
||||
|
||||
go 1.23.1
|
||||
|
||||
require (
|
||||
fyne.io/fyne/v2 v2.7.0
|
||||
github.com/flynn/noise v1.0.0
|
||||
github.com/refraction-networking/utls v1.6.6
|
||||
github.com/xjasonlyu/tun2socks/v2 v2.6.0
|
||||
github.com/xtaci/kcp-go/v5 v5.6.8
|
||||
github.com/xtaci/smux v1.5.24
|
||||
golang.org/x/crypto v0.38.0
|
||||
golang.org/x/net v0.40.0
|
||||
)
|
||||
|
||||
require (
|
||||
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/fredbi/uri v1.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fyne-io/gl-js v0.2.0 // indirect
|
||||
github.com/fyne-io/glfw-js v0.3.0 // indirect
|
||||
github.com/fyne-io/image v0.1.1 // indirect
|
||||
github.com/fyne-io/oksvg v0.2.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.1 // indirect
|
||||
github.com/go-chi/cors v1.2.1 // indirect
|
||||
github.com/go-chi/render v1.0.3 // indirect
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
|
||||
github.com/go-gost/relay v0.5.0 // indirect
|
||||
github.com/go-text/render v0.2.0 // indirect
|
||||
github.com/go-text/typesetting v0.2.1 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/schema v1.4.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
|
||||
github.com/hack-pad/safejs v0.1.0 // indirect
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||
github.com/klauspost/reedsolomon v1.12.0 // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rymdport/portal v0.4.2 // indirect
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
github.com/templexxx/cpu v0.1.0 // indirect
|
||||
github.com/templexxx/xorsimd v0.4.2 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/yuin/goldmark v1.7.8 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/image v0.24.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20250523182742-eede7a881b20 // indirect
|
||||
)
|
||||
227
go.sum
Normal file
227
go.sum
Normal file
@@ -0,0 +1,227 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
fyne.io/fyne/v2 v2.7.0 h1:GvZSpE3X0liU/fqstInVvRsaboIVpIWQ4/sfjDGIGGQ=
|
||||
fyne.io/fyne/v2 v2.7.0/go.mod h1:xClVlrhxl7D+LT+BWYmcrW4Nf+dJTvkhnPgji7spAwE=
|
||||
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 h1:eA5/u2XRd8OUkoMqEv3IBlFYSruNlXD8bRHDiqm0VNI=
|
||||
fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
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/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
||||
github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ=
|
||||
github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
|
||||
github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko=
|
||||
github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
|
||||
github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
|
||||
github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk=
|
||||
github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
|
||||
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
|
||||
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
|
||||
github.com/fyne-io/oksvg v0.2.0 h1:mxcGU2dx6nwjJsSA9PCYZDuoAcsZ/OuJlvg/Q9Njfo8=
|
||||
github.com/fyne-io/oksvg v0.2.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
|
||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gost/relay v0.5.0 h1:JG1tgy/KWiVXS0ukuVXvbM0kbYuJTWxYpJ5JwzsCf/c=
|
||||
github.com/go-gost/relay v0.5.0/go.mod h1:lcX+23LCQ3khIeASBo+tJ/WbwXFO32/N5YN6ucuYTG8=
|
||||
github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
|
||||
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
|
||||
github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
|
||||
github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
|
||||
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
|
||||
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
|
||||
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
|
||||
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
|
||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/reedsolomon v1.12.0 h1:I5FEp3xSwVCcEh3F5A7dofEfhXdF/bWhQWPH+XwBFno=
|
||||
github.com/klauspost/reedsolomon v1.12.0/go.mod h1:EPLZJeh4l27pUGC3aXOjheaoh1I9yut7xTURiW3LQ9Y=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
||||
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
||||
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/refraction-networking/utls v1.6.6 h1:igFsYBUJPYM8Rno9xUuDoM5GQrVEqY4llzEXOkL43Ig=
|
||||
github.com/refraction-networking/utls v1.6.6/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
|
||||
github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU=
|
||||
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/templexxx/cpu v0.1.0 h1:wVM+WIJP2nYaxVxqgHPD4wGA2aJ9rvrQRV8CvFzNb40=
|
||||
github.com/templexxx/cpu v0.1.0/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
|
||||
github.com/templexxx/xorsimd v0.4.2 h1:ocZZ+Nvu65LGHmCLZ7OoCtg8Fx8jnHKK37SjvngUoVI=
|
||||
github.com/templexxx/xorsimd v0.4.2/go.mod h1:HgwaPoDREdi6OnULpSfxhzaiiSUY4Fi3JPn1wpt28NI=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
github.com/xjasonlyu/tun2socks/v2 v2.6.0 h1:gI9saJT3XgH4e6v9jBuHRLwK7l3aN9YFWec/SsDTDx4=
|
||||
github.com/xjasonlyu/tun2socks/v2 v2.6.0/go.mod h1:35AwqxIxnMkfBfT0UJ1Lku7PZm2ZiZJ8sxHyp0gt1yw=
|
||||
github.com/xtaci/kcp-go/v5 v5.6.8 h1:jlI/0jAyjoOjT/SaGB58s4bQMJiNS41A2RKzR6TMWeI=
|
||||
github.com/xtaci/kcp-go/v5 v5.6.8/go.mod h1:oE9j2NVqAkuKO5o8ByKGch3vgVX3BNf8zqP8JiGq0bM=
|
||||
github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM=
|
||||
github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE=
|
||||
github.com/xtaci/smux v1.5.24 h1:77emW9dtnOxxOQ5ltR+8BbsX1kzcOxQ5gB+aaV9hXOY=
|
||||
github.com/xtaci/smux v1.5.24/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY=
|
||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
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-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gvisor.dev/gvisor v0.0.0-20250523182742-eede7a881b20 h1:0DxLu8hxI1OGp1qVRPqNd+2k1a7hMNUNqbZG0IrtKlM=
|
||||
gvisor.dev/gvisor v0.0.0-20250523182742-eede7a881b20/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
57
internal/app/app.go
Normal file
57
internal/app/app.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"socksrevivepc/internal/config"
|
||||
"socksrevivepc/internal/engine"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
Root string
|
||||
Store *config.Store
|
||||
Engine *engine.Manager
|
||||
}
|
||||
|
||||
func New() (*App, error) {
|
||||
root, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, d := range []string{"configs", "logs", filepath.Join("tools", "dnstt"), filepath.Join("tools", "xray"), filepath.Join("tools", "wintun")} {
|
||||
if err := os.MkdirAll(filepath.Join(root, d), 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
store, err := config.NewStore(filepath.Join(root, "profiles"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mgr := engine.NewManager(root)
|
||||
if err := removeBundledExamples(store); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
app := &App{Root: root, Store: store, Engine: mgr}
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func removeBundledExamples(store *config.Store) error {
|
||||
list, err := store.List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
exampleNames := map[string]bool{
|
||||
"Example - Direct SSH": true,
|
||||
"Example - Payload + SSL": true,
|
||||
"Example - Xray Core": true,
|
||||
}
|
||||
for _, p := range list {
|
||||
if exampleNames[p.Name] {
|
||||
if err := store.Delete(p.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
477
internal/config/config.go
Normal file
477
internal/config/config.go
Normal file
@@ -0,0 +1,477 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/rand"
|
||||
"encoding/gob"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Mode string
|
||||
|
||||
const (
|
||||
ModeDirect Mode = "direct"
|
||||
ModePayload Mode = "payload"
|
||||
ModeSSL Mode = "ssl"
|
||||
ModePayloadSSL Mode = "payload_ssl"
|
||||
ModeDNSTT Mode = "dnstt"
|
||||
ModeXray Mode = "xray"
|
||||
)
|
||||
|
||||
const (
|
||||
ProfileExtension = ".srpc"
|
||||
profileMagic = "SRPC\x01"
|
||||
)
|
||||
|
||||
type Profile struct {
|
||||
ID string
|
||||
Name string
|
||||
Mode Mode
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
|
||||
SSH SSHConfig
|
||||
Proxy ProxyConfig
|
||||
Payload PayloadConfig
|
||||
TLS TLSConfig
|
||||
DNSTT DNSTTConfig
|
||||
Xray XrayConfig
|
||||
UDPGW UDPGWConfig
|
||||
Reconnect ReconnectConfig
|
||||
Local LocalConfig
|
||||
Tun TunConfig
|
||||
}
|
||||
|
||||
type SSHConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
KeepAliveSeconds int
|
||||
HandshakeTimeoutMs int
|
||||
}
|
||||
|
||||
type ProxyConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
type PayloadConfig struct {
|
||||
Text string
|
||||
WaitForResponse bool
|
||||
AcceptAnyStatus bool
|
||||
ResponseTimeoutMs int
|
||||
SplitDelayMs int
|
||||
}
|
||||
|
||||
type TLSConfig struct {
|
||||
Enabled bool
|
||||
Host string
|
||||
Port int
|
||||
ServerName string
|
||||
InsecureSkipVerify bool
|
||||
}
|
||||
|
||||
type DNSTTConfig struct {
|
||||
Enabled bool
|
||||
UseEmbedded bool
|
||||
ResolverType string
|
||||
ResolverAddress string
|
||||
Domain string
|
||||
PublicKey string
|
||||
UTLSDistribution string
|
||||
Executable string
|
||||
Args []string
|
||||
LocalSSHHost string
|
||||
LocalSSHPort int
|
||||
StartupTimeoutMs int
|
||||
}
|
||||
|
||||
type XrayConfig struct {
|
||||
Executable string
|
||||
Args []string
|
||||
ConfigPath string
|
||||
LocalSocksHost string
|
||||
LocalSocksPort int
|
||||
StartupTimeoutMs int
|
||||
}
|
||||
|
||||
type UDPGWConfig struct {
|
||||
Enabled bool
|
||||
Host string
|
||||
Port int
|
||||
Protocol string
|
||||
}
|
||||
|
||||
type ReconnectConfig struct {
|
||||
Enabled bool
|
||||
DelaySeconds int
|
||||
MaxRetries int
|
||||
CheckIntervalSeconds int
|
||||
}
|
||||
|
||||
type LocalConfig struct {
|
||||
SocksHost string
|
||||
SocksPort int
|
||||
}
|
||||
|
||||
type TunConfig struct {
|
||||
Enabled bool
|
||||
Device string
|
||||
InterfaceName string
|
||||
MTU int
|
||||
Gateway string
|
||||
CIDR string
|
||||
DNS []string
|
||||
RouteAll bool
|
||||
|
||||
// IPv6 is optional because many SSH/DNSTT/UDPGW servers only have IPv4
|
||||
// egress. When IPv6Enabled is false, AllowIPv6Leak controls whether the
|
||||
// app should leave the normal Windows/Linux IPv6 route untouched.
|
||||
IPv6Enabled bool
|
||||
IPv6CIDR string
|
||||
IPv6DNS []string
|
||||
AllowIPv6Leak bool
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
Dir string
|
||||
}
|
||||
|
||||
func NewStore(dir string) (*Store, error) {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Store{Dir: dir}, nil
|
||||
}
|
||||
|
||||
func (s *Store) List() ([]Profile, error) {
|
||||
entries, err := os.ReadDir(s.Dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []Profile
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(strings.ToLower(e.Name()), ProfileExtension) {
|
||||
continue
|
||||
}
|
||||
p, err := s.Load(strings.TrimSuffix(e.Name(), ProfileExtension))
|
||||
if err == nil {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)
|
||||
})
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Store) Load(id string) (Profile, error) {
|
||||
var p Profile
|
||||
b, err := os.ReadFile(filepath.Join(s.Dir, safeID(id)+ProfileExtension))
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p, err = DecodeProfileFile(b)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
ApplyDefaults(&p)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (s *Store) Save(p Profile) (Profile, error) {
|
||||
if p.ID == "" {
|
||||
p.ID = randomID()
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
if p.CreatedAt.IsZero() {
|
||||
p.CreatedAt = now
|
||||
}
|
||||
p.UpdatedAt = now
|
||||
ApplyDefaults(&p)
|
||||
if err := Validate(p); err != nil {
|
||||
return p, err
|
||||
}
|
||||
b, err := EncodeProfileFile(p)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
return p, os.WriteFile(filepath.Join(s.Dir, safeID(p.ID)+ProfileExtension), b, 0o600)
|
||||
}
|
||||
|
||||
func (s *Store) Delete(id string) error {
|
||||
return os.Remove(filepath.Join(s.Dir, safeID(id)+ProfileExtension))
|
||||
}
|
||||
|
||||
func EncodeProfileFile(p Profile) ([]byte, error) {
|
||||
ApplyDefaults(&p)
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(profileMagic)
|
||||
gz := gzip.NewWriter(&buf)
|
||||
if err := gob.NewEncoder(gz).Encode(p); err != nil {
|
||||
_ = gz.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := gz.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func DecodeProfileFile(b []byte) (Profile, error) {
|
||||
var p Profile
|
||||
if !bytes.HasPrefix(b, []byte(profileMagic)) {
|
||||
return p, errors.New("invalid SocksRevive PC profile file")
|
||||
}
|
||||
gz, err := gzip.NewReader(bytes.NewReader(b[len(profileMagic):]))
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
defer gz.Close()
|
||||
payload, err := io.ReadAll(gz)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
if err := gob.NewDecoder(bytes.NewReader(payload)).Decode(&p); err != nil {
|
||||
return p, err
|
||||
}
|
||||
ApplyDefaults(&p)
|
||||
return p, Validate(p)
|
||||
}
|
||||
|
||||
func Validate(p Profile) error {
|
||||
if strings.TrimSpace(p.Name) == "" {
|
||||
return errors.New("profile name is required")
|
||||
}
|
||||
switch p.Mode {
|
||||
case ModeDirect, ModePayload, ModeSSL, ModePayloadSSL, ModeDNSTT, ModeXray:
|
||||
default:
|
||||
return fmt.Errorf("unknown mode %q", p.Mode)
|
||||
}
|
||||
if p.Mode != ModeXray {
|
||||
if p.SSH.Username == "" || p.SSH.Password == "" {
|
||||
return errors.New("ssh username/password are required")
|
||||
}
|
||||
if p.Mode != ModeDNSTT && (p.SSH.Host == "" || p.SSH.Port <= 0) {
|
||||
return errors.New("ssh host/port are required")
|
||||
}
|
||||
}
|
||||
if (p.Mode == ModePayload || p.Mode == ModePayloadSSL) && strings.TrimSpace(p.Payload.Text) == "" {
|
||||
return errors.New("payload text is required for payload modes")
|
||||
}
|
||||
if p.Mode == ModeXray && strings.TrimSpace(p.Xray.Executable) == "" {
|
||||
return errors.New("xray executable path is required")
|
||||
}
|
||||
if p.Mode == ModeDNSTT {
|
||||
if p.DNSTT.UseEmbedded {
|
||||
if strings.TrimSpace(p.DNSTT.ResolverType) == "" || strings.TrimSpace(p.DNSTT.ResolverAddress) == "" {
|
||||
return errors.New("dnstt resolver type/address are required")
|
||||
}
|
||||
if strings.TrimSpace(p.DNSTT.Domain) == "" {
|
||||
return errors.New("dnstt domain is required")
|
||||
}
|
||||
if strings.TrimSpace(p.DNSTT.PublicKey) == "" {
|
||||
return errors.New("dnstt public key is required")
|
||||
}
|
||||
} else if strings.TrimSpace(p.DNSTT.Executable) == "" {
|
||||
return errors.New("dnstt executable path is required when embedded dnstt is disabled")
|
||||
}
|
||||
}
|
||||
if p.UDPGW.Enabled {
|
||||
if strings.TrimSpace(p.UDPGW.Host) == "" {
|
||||
return errors.New("udpgw host is required when udpgw is enabled")
|
||||
}
|
||||
if p.UDPGW.Port <= 0 || p.UDPGW.Port > 65535 {
|
||||
return errors.New("udpgw port must be between 1 and 65535")
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(p.UDPGW.Protocol)) {
|
||||
case "", "badvpn", "legacy":
|
||||
default:
|
||||
return errors.New("udpgw protocol must be badvpn or legacy")
|
||||
}
|
||||
}
|
||||
if p.Local.SocksPort <= 0 || p.Local.SocksPort > 65535 {
|
||||
return errors.New("local socks port must be between 1 and 65535")
|
||||
}
|
||||
if p.Tun.Enabled && p.Tun.IPv6Enabled {
|
||||
ip, _, err := net.ParseCIDR(strings.TrimSpace(p.Tun.IPv6CIDR))
|
||||
if err != nil || ip == nil || ip.To4() != nil {
|
||||
return errors.New("IPv6 CIDR must be a valid IPv6 CIDR, for example fd00:534f:434b::1/64")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ApplyDefaults(p *Profile) {
|
||||
if p.ID == "" {
|
||||
p.ID = randomID()
|
||||
}
|
||||
if p.Mode == "" {
|
||||
p.Mode = ModeDirect
|
||||
}
|
||||
if p.SSH.Port == 0 {
|
||||
p.SSH.Port = 22
|
||||
}
|
||||
if p.SSH.KeepAliveSeconds == 0 {
|
||||
p.SSH.KeepAliveSeconds = 20
|
||||
}
|
||||
if p.SSH.HandshakeTimeoutMs == 0 {
|
||||
p.SSH.HandshakeTimeoutMs = 15000
|
||||
}
|
||||
if p.Payload.Text == "" {
|
||||
p.Payload.Text = "CONNECT [host]:[port] HTTP/1.1[crlf]Host: [host][crlf]User-Agent: SocksRevivePC[crlf][crlf]"
|
||||
}
|
||||
if p.Payload.ResponseTimeoutMs == 0 {
|
||||
p.Payload.ResponseTimeoutMs = 8000
|
||||
}
|
||||
if p.Payload.SplitDelayMs == 0 {
|
||||
p.Payload.SplitDelayMs = 120
|
||||
}
|
||||
if p.TLS.Port == 0 {
|
||||
p.TLS.Port = 443
|
||||
}
|
||||
if p.Reconnect.DelaySeconds == 0 {
|
||||
p.Reconnect.DelaySeconds = 3
|
||||
}
|
||||
if p.Reconnect.CheckIntervalSeconds == 0 {
|
||||
p.Reconnect.CheckIntervalSeconds = 10
|
||||
}
|
||||
if p.Local.SocksHost == "" {
|
||||
p.Local.SocksHost = "127.0.0.1"
|
||||
}
|
||||
if p.Local.SocksPort == 0 {
|
||||
p.Local.SocksPort = 10809
|
||||
}
|
||||
if p.DNSTT.LocalSSHHost == "" {
|
||||
p.DNSTT.LocalSSHHost = "127.0.0.1"
|
||||
}
|
||||
if p.DNSTT.LocalSSHPort == 0 {
|
||||
p.DNSTT.LocalSSHPort = 2222
|
||||
}
|
||||
if p.DNSTT.StartupTimeoutMs == 0 {
|
||||
p.DNSTT.StartupTimeoutMs = 5000
|
||||
}
|
||||
if p.DNSTT.ResolverType == "" {
|
||||
p.DNSTT.ResolverType = "doh"
|
||||
}
|
||||
if p.DNSTT.UTLSDistribution == "" {
|
||||
p.DNSTT.UTLSDistribution = "4*random,3*Firefox_120,1*Firefox_105,3*Chrome_120,1*Chrome_102,1*iOS_14,1*iOS_13"
|
||||
}
|
||||
if !p.DNSTT.UseEmbedded && p.DNSTT.Executable == "" && len(p.DNSTT.Args) == 0 {
|
||||
// New profiles use the embedded DNSTT client. Old imported profiles can
|
||||
// still disable UseEmbedded and point to an external executable.
|
||||
p.DNSTT.UseEmbedded = true
|
||||
}
|
||||
if p.DNSTT.Executable == "" {
|
||||
p.DNSTT.Executable = defaultToolPath("dnstt", "dnstt-client")
|
||||
}
|
||||
if p.Xray.LocalSocksHost == "" {
|
||||
p.Xray.LocalSocksHost = "127.0.0.1"
|
||||
}
|
||||
if p.Xray.LocalSocksPort == 0 {
|
||||
p.Xray.LocalSocksPort = 10808
|
||||
}
|
||||
if p.Xray.ConfigPath == "" {
|
||||
p.Xray.ConfigPath = "configs/xray.json"
|
||||
}
|
||||
if len(p.Xray.Args) == 0 {
|
||||
p.Xray.Args = []string{"run", "-config", p.Xray.ConfigPath}
|
||||
}
|
||||
if p.Xray.StartupTimeoutMs == 0 {
|
||||
p.Xray.StartupTimeoutMs = 2500
|
||||
}
|
||||
if p.Xray.Executable == "" {
|
||||
p.Xray.Executable = defaultToolPath("xray", "xray")
|
||||
}
|
||||
if p.UDPGW.Host == "" {
|
||||
p.UDPGW.Host = "127.0.0.1"
|
||||
}
|
||||
if p.UDPGW.Port == 0 {
|
||||
p.UDPGW.Port = 7400
|
||||
}
|
||||
if strings.TrimSpace(p.UDPGW.Protocol) == "" {
|
||||
// BadVPN is the Android-compatible UDPGW framing. The old PC-only
|
||||
// experimental frame is still available as "legacy" for older tests.
|
||||
p.UDPGW.Protocol = "badvpn"
|
||||
}
|
||||
if p.Tun.Device == "" {
|
||||
if runtime.GOOS == "windows" {
|
||||
p.Tun.Device = "wintun"
|
||||
p.Tun.InterfaceName = "wintun"
|
||||
} else {
|
||||
p.Tun.Device = "tun://socksrevive0"
|
||||
p.Tun.InterfaceName = "socksrevive0"
|
||||
}
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
// tun2socks v2 expects the Windows device model to be just "wintun".
|
||||
// The old value "wintun://SocksRevive" made the engine treat
|
||||
// "SocksRevive" as a network interface and crash with
|
||||
// "route ip+net: no such network interface" on many PCs.
|
||||
p.Tun.Device = "wintun"
|
||||
p.Tun.InterfaceName = "wintun"
|
||||
}
|
||||
if p.Tun.InterfaceName == "" {
|
||||
if strings.HasPrefix(p.Tun.Device, "tun://") {
|
||||
p.Tun.InterfaceName = strings.TrimPrefix(p.Tun.Device, "tun://")
|
||||
} else if strings.HasPrefix(p.Tun.Device, "wintun://") {
|
||||
p.Tun.InterfaceName = strings.TrimPrefix(p.Tun.Device, "wintun://")
|
||||
} else {
|
||||
p.Tun.InterfaceName = p.Tun.Device
|
||||
}
|
||||
}
|
||||
if p.Tun.MTU == 0 {
|
||||
p.Tun.MTU = 1500
|
||||
}
|
||||
if p.Tun.Gateway == "" {
|
||||
p.Tun.Gateway = "198.18.0.1"
|
||||
}
|
||||
if p.Tun.CIDR == "" {
|
||||
p.Tun.CIDR = "198.18.0.1/15"
|
||||
}
|
||||
if len(p.Tun.DNS) == 0 {
|
||||
p.Tun.DNS = []string{"1.1.1.1", "8.8.8.8"}
|
||||
}
|
||||
if p.Tun.IPv6CIDR == "" {
|
||||
p.Tun.IPv6CIDR = "fd00:534f:434b::1/64"
|
||||
}
|
||||
if len(p.Tun.IPv6DNS) == 0 {
|
||||
p.Tun.IPv6DNS = []string{"2606:4700:4700::1111", "2001:4860:4860::8888"}
|
||||
}
|
||||
}
|
||||
|
||||
func defaultToolPath(folder, base string) string {
|
||||
exe := base
|
||||
if runtime.GOOS == "windows" {
|
||||
exe += ".exe"
|
||||
}
|
||||
return filepath.Join("tools", folder, exe)
|
||||
}
|
||||
|
||||
func randomID() string {
|
||||
b := make([]byte, 8)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return fmt.Sprintf("p%d", time.Now().UnixNano())
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func safeID(id string) string {
|
||||
id = strings.TrimSpace(id)
|
||||
id = strings.ReplaceAll(id, "/", "_")
|
||||
id = strings.ReplaceAll(id, "\\", "_")
|
||||
id = strings.ReplaceAll(id, "..", "_")
|
||||
return id
|
||||
}
|
||||
73
internal/crash/crash.go
Normal file
73
internal/crash/crash.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package crash
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var fileMu sync.Mutex
|
||||
|
||||
// AttachLog mirrors the standard logger into logs/runtime.log. It is useful for
|
||||
// GUI builds on Windows where there is no console attached.
|
||||
func AttachLog(root string) func() {
|
||||
if root == "" {
|
||||
root = "."
|
||||
}
|
||||
logDir := filepath.Join(root, "logs")
|
||||
_ = os.MkdirAll(logDir, 0o755)
|
||||
path := filepath.Join(logDir, "runtime.log")
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||
if err != nil {
|
||||
return func() {}
|
||||
}
|
||||
log.SetOutput(f)
|
||||
log.SetFlags(log.LstdFlags | log.Lmicroseconds | log.Lshortfile)
|
||||
log.Printf("SocksRevivePC started")
|
||||
return func() {
|
||||
log.Printf("SocksRevivePC stopped")
|
||||
_ = f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Recover writes any Go panic to logs/crash.log. This does not hide the crash;
|
||||
// it only makes GUI builds debuggable when Windows closes the app silently.
|
||||
func Recover(root string) {
|
||||
if v := recover(); v != nil {
|
||||
Write(root, "panic", v)
|
||||
}
|
||||
}
|
||||
|
||||
// Go starts a goroutine with panic logging. Use this for goroutines owned by the
|
||||
// app so background failures do not vanish without a stack trace.
|
||||
func Go(root string, fn func()) {
|
||||
go func() {
|
||||
defer Recover(root)
|
||||
fn()
|
||||
}()
|
||||
}
|
||||
|
||||
func Write(root, kind string, value any) {
|
||||
if root == "" {
|
||||
root = "."
|
||||
}
|
||||
fileMu.Lock()
|
||||
defer fileMu.Unlock()
|
||||
debug.SetTraceback("all")
|
||||
logDir := filepath.Join(root, "logs")
|
||||
_ = os.MkdirAll(logDir, 0o755)
|
||||
path := filepath.Join(logDir, "crash.log")
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
_, _ = fmt.Fprintf(f, "\n===== %s =====\n", time.Now().Format(time.RFC3339Nano))
|
||||
_, _ = fmt.Fprintf(f, "%s: %v\n\n", kind, value)
|
||||
_, _ = f.Write(debug.Stack())
|
||||
_, _ = f.WriteString("\n")
|
||||
}
|
||||
276
internal/dnsttclient/api.go
Normal file
276
internal/dnsttclient/api.go
Normal file
@@ -0,0 +1,276 @@
|
||||
package dnsttclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
utls "github.com/refraction-networking/utls"
|
||||
"github.com/xtaci/kcp-go/v5"
|
||||
"github.com/xtaci/smux"
|
||||
"socksrevivepc/internal/dnsttcore/dns"
|
||||
"socksrevivepc/internal/dnsttcore/noise"
|
||||
"socksrevivepc/internal/dnsttcore/turbotunnel"
|
||||
)
|
||||
|
||||
// Options configures the embedded DNSTT client.
|
||||
type Options struct {
|
||||
ResolverType string
|
||||
ResolverAddress string
|
||||
PublicKeyHex string
|
||||
Domain string
|
||||
LocalAddress string
|
||||
UTLSDistribution string
|
||||
StartupTimeout time.Duration
|
||||
LogWriter io.Writer
|
||||
}
|
||||
|
||||
// Client is a running embedded DNSTT client instance.
|
||||
type Client struct {
|
||||
cancel context.CancelFunc
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Stop shuts down the listener and DNS transport.
|
||||
func (c *Client) Stop() {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
c.cancel()
|
||||
select {
|
||||
case <-c.done:
|
||||
case <-time.After(3 * time.Second):
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts an embedded DNSTT client and waits until the local TCP listener
|
||||
// is open. It does not require dnstt-client.exe.
|
||||
func Start(parent context.Context, opts Options) (*Client, error) {
|
||||
if opts.LogWriter != nil {
|
||||
log.SetOutput(opts.LogWriter)
|
||||
log.SetFlags(log.LstdFlags | log.LUTC)
|
||||
}
|
||||
if opts.StartupTimeout <= 0 {
|
||||
opts.StartupTimeout = 5 * time.Second
|
||||
}
|
||||
ctx, cancel := context.WithCancel(parent)
|
||||
client := &Client{cancel: cancel, done: make(chan struct{})}
|
||||
ready := make(chan struct{})
|
||||
errCh := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
defer close(client.done)
|
||||
errCh <- runOptions(ctx, opts, ready)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ready:
|
||||
return client, nil
|
||||
case err := <-errCh:
|
||||
cancel()
|
||||
if err == nil {
|
||||
err = fmt.Errorf("embedded dnstt stopped during startup")
|
||||
}
|
||||
return nil, err
|
||||
case <-time.After(opts.StartupTimeout):
|
||||
// Keep the client alive. The local listener usually opens first, but on
|
||||
// slow networks the Noise/smux session can need a little more time.
|
||||
return client, nil
|
||||
case <-parent.Done():
|
||||
cancel()
|
||||
return nil, parent.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func runOptions(ctx context.Context, opts Options, ready chan<- struct{}) error {
|
||||
resolverType := strings.ToLower(strings.TrimSpace(opts.ResolverType))
|
||||
resolverAddress := strings.TrimSpace(opts.ResolverAddress)
|
||||
if resolverType == "" {
|
||||
resolverType = "doh"
|
||||
}
|
||||
if resolverAddress == "" {
|
||||
return fmt.Errorf("dnstt resolver is required")
|
||||
}
|
||||
if opts.PublicKeyHex == "" {
|
||||
return fmt.Errorf("dnstt public key is required")
|
||||
}
|
||||
if opts.Domain == "" {
|
||||
return fmt.Errorf("dnstt domain is required")
|
||||
}
|
||||
if opts.LocalAddress == "" {
|
||||
opts.LocalAddress = "127.0.0.1:2222"
|
||||
}
|
||||
if opts.UTLSDistribution == "" {
|
||||
opts.UTLSDistribution = "4*random,3*Firefox_120,1*Firefox_105,3*Chrome_120,1*Chrome_102,1*iOS_14,1*iOS_13"
|
||||
}
|
||||
|
||||
domain, err := dns.ParseName(opts.Domain)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid dnstt domain: %w", err)
|
||||
}
|
||||
localAddr, err := net.ResolveTCPAddr("tcp", opts.LocalAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid dnstt local address: %w", err)
|
||||
}
|
||||
pubkey, err := noise.DecodeKey(opts.PublicKeyHex)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dnstt public key format error: %w", err)
|
||||
}
|
||||
|
||||
tlsConfig, err := loadTLSConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("dnstt TLS config error: %w", err)
|
||||
}
|
||||
utlsClientHelloID, err := sampleUTLSDistribution(opts.UTLSDistribution)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dnstt uTLS profile error: %w", err)
|
||||
}
|
||||
if utlsClientHelloID != nil {
|
||||
log.Printf("dnstt uTLS fingerprint %s %s", utlsClientHelloID.Client, utlsClientHelloID.Version)
|
||||
}
|
||||
|
||||
remoteAddr, pconn, err := makePacketConn(resolverType, resolverAddress, tlsConfig, utlsClientHelloID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pconn = NewDNSPacketConn(pconn, remoteAddr, domain)
|
||||
return runContext(ctx, pubkey, domain, localAddr, remoteAddr, pconn, ready)
|
||||
}
|
||||
|
||||
func makePacketConn(resolverType, resolverAddress string, tlsConfig *tls.Config, utlsClientHelloID *utls.ClientHelloID) (net.Addr, net.PacketConn, error) {
|
||||
switch resolverType {
|
||||
case "doh", "https":
|
||||
addr := turbotunnel.DummyAddr{}
|
||||
var rt http.RoundTripper
|
||||
if utlsClientHelloID == nil {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.Proxy = nil
|
||||
transport.TLSClientConfig = tlsConfig.Clone()
|
||||
baseDialContext := transport.DialContext
|
||||
if baseDialContext == nil {
|
||||
baseDialContext = (&net.Dialer{}).DialContext
|
||||
}
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
if network == "tcp" {
|
||||
resolvedAddr, _, err := resolveAddrIPv4(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addr = resolvedAddr
|
||||
network = "tcp4"
|
||||
}
|
||||
return baseDialContext(ctx, network, addr)
|
||||
}
|
||||
rt = transport
|
||||
} else {
|
||||
utlsConfig := &utls.Config{RootCAs: tlsConfig.RootCAs, MinVersion: tlsConfig.MinVersion}
|
||||
rt = NewUTLSRoundTripper(utlsConfig, utlsClientHelloID, true)
|
||||
}
|
||||
pconn, err := NewHTTPPacketConn(rt, resolverAddress, 32)
|
||||
return addr, pconn, err
|
||||
case "dot", "tls":
|
||||
addr := turbotunnel.DummyAddr{}
|
||||
var dialTLSContext func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
if utlsClientHelloID == nil {
|
||||
dialTLSContext = (&tls.Dialer{Config: tlsConfig.Clone()}).DialContext
|
||||
} else {
|
||||
utlsConfig := &utls.Config{RootCAs: tlsConfig.RootCAs, MinVersion: tlsConfig.MinVersion}
|
||||
dialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return utlsDialContext(ctx, network, addr, utlsConfig, utlsClientHelloID)
|
||||
}
|
||||
}
|
||||
pconn, err := NewTLSPacketConn(resolverAddress, dialTLSContext)
|
||||
return addr, pconn, err
|
||||
case "udp", "dns":
|
||||
addr, err := net.ResolveUDPAddr("udp", resolverAddress)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
pconn, err := net.ListenUDP("udp", nil)
|
||||
return addr, pconn, err
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("unknown dnstt resolver type %q", resolverType)
|
||||
}
|
||||
}
|
||||
|
||||
func runContext(ctx context.Context, pubkey []byte, domain dns.Name, localAddr *net.TCPAddr, remoteAddr net.Addr, pconn net.PacketConn, ready chan<- struct{}) error {
|
||||
defer pconn.Close()
|
||||
|
||||
ln, err := net.ListenTCP("tcp", localAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening dnstt local listener: %w", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
_ = ln.Close()
|
||||
_ = pconn.Close()
|
||||
}()
|
||||
close(ready)
|
||||
log.Printf("dnstt local listener ready at %s", ln.Addr())
|
||||
|
||||
mtu := dnsNameCapacity(domain) - 8 - 1 - numPadding - 1
|
||||
if mtu < 80 {
|
||||
return fmt.Errorf("domain %s leaves only %d bytes for payload", domain, mtu)
|
||||
}
|
||||
log.Printf("dnstt effective MTU %d", mtu)
|
||||
|
||||
conn, err := kcp.NewConn2(remoteAddr, nil, 0, 0, pconn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening dnstt KCP connection: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
log.Printf("end dnstt session %08x", conn.GetConv())
|
||||
conn.Close()
|
||||
}()
|
||||
log.Printf("begin dnstt session %08x", conn.GetConv())
|
||||
conn.SetStreamMode(true)
|
||||
conn.SetNoDelay(0, 0, 0, 1)
|
||||
conn.SetWindowSize(turbotunnel.QueueSize/2, turbotunnel.QueueSize/2)
|
||||
if rc := conn.SetMtu(mtu); !rc {
|
||||
return fmt.Errorf("setting dnstt MTU failed")
|
||||
}
|
||||
|
||||
rw, err := noise.NewClient(conn, pubkey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
smuxConfig := smux.DefaultConfig()
|
||||
smuxConfig.Version = 2
|
||||
smuxConfig.KeepAliveTimeout = idleTimeout
|
||||
smuxConfig.MaxStreamBuffer = 1 * 1024 * 1024
|
||||
sess, err := smux.Client(rw, smuxConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening dnstt smux session: %w", err)
|
||||
}
|
||||
defer sess.Close()
|
||||
|
||||
for {
|
||||
local, err := ln.Accept()
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
if err, ok := err.(net.Error); ok && err.Temporary() {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
defer local.Close()
|
||||
err := handle(local.(*net.TCPConn), sess, conn.GetConv())
|
||||
if err != nil {
|
||||
log.Printf("dnstt handle: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
22
internal/dnsttclient/certpool.go
Normal file
22
internal/dnsttclient/certpool.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package dnsttclient
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
)
|
||||
|
||||
func loadTLSConfig() (*tls.Config, error) {
|
||||
pool, err := x509.SystemCertPool()
|
||||
if err != nil {
|
||||
pool = nil
|
||||
}
|
||||
|
||||
config := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
if pool != nil {
|
||||
config.RootCAs = pool
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
407
internal/dnsttclient/dns.go
Normal file
407
internal/dnsttclient/dns.go
Normal file
@@ -0,0 +1,407 @@
|
||||
package dnsttclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/base32"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"socksrevivepc/internal/dnsttcore/dns"
|
||||
"socksrevivepc/internal/dnsttcore/turbotunnel"
|
||||
)
|
||||
|
||||
const (
|
||||
// How many bytes of random padding to insert into queries.
|
||||
numPadding = 3
|
||||
// In an otherwise empty polling query, insert even more random padding,
|
||||
// to reduce the chance of a cache hit. Cannot be greater than 31,
|
||||
// because the prefix codes indicating padding start at 224.
|
||||
numPaddingForPoll = 8
|
||||
|
||||
// sendLoop has a poll timer that automatically sends an empty polling
|
||||
// query when a certain amount of time has elapsed without a send. The
|
||||
// poll timer is initially set to initPollDelay. It increases by a
|
||||
// factor of pollDelayMultiplier every time the poll timer expires, up
|
||||
// to a maximum of maxPollDelay. The poll timer is reset to
|
||||
// initPollDelay whenever an a send occurs that is not the result of the
|
||||
// poll timer expiring.
|
||||
initPollDelay = 500 * time.Millisecond
|
||||
maxPollDelay = 10 * time.Second
|
||||
pollDelayMultiplier = 2.0
|
||||
|
||||
// A limit on the number of empty poll requests we may send in a burst
|
||||
// as a result of receiving data.
|
||||
pollLimit = 16
|
||||
)
|
||||
|
||||
// base32Encoding is a base32 encoding without padding.
|
||||
var base32Encoding = base32.StdEncoding.WithPadding(base32.NoPadding)
|
||||
|
||||
// DNSPacketConn provides a packet-sending and -receiving interface over various
|
||||
// forms of DNS. It handles the details of how packets and padding are encoded
|
||||
// as a DNS name in the Question section of an upstream query, and as a TXT RR
|
||||
// in downstream responses.
|
||||
//
|
||||
// DNSPacketConn does not handle the mechanics of actually sending and receiving
|
||||
// encoded DNS messages. That is rather the responsibility of some other
|
||||
// net.PacketConn such as net.UDPConn, HTTPPacketConn, or TLSPacketConn, one of
|
||||
// which must be provided to NewDNSPacketConn.
|
||||
//
|
||||
// We don't have a need to match up a query and a response by ID. Queries and
|
||||
// responses are vehicles for carrying data and for our purposes don't need to
|
||||
// be correlated. When sending a query, we generate a random ID, and when
|
||||
// receiving a response, we ignore the ID.
|
||||
type DNSPacketConn struct {
|
||||
clientID turbotunnel.ClientID
|
||||
domain dns.Name
|
||||
// Sending on pollChan permits sendLoop to send an empty polling query.
|
||||
// sendLoop also does its own polling according to a time schedule.
|
||||
pollChan chan struct{}
|
||||
// QueuePacketConn is the direct receiver of ReadFrom and WriteTo calls.
|
||||
// recvLoop and sendLoop take the messages out of the receive and send
|
||||
// queues and actually put them on the network.
|
||||
*turbotunnel.QueuePacketConn
|
||||
}
|
||||
|
||||
// NewDNSPacketConn creates a new DNSPacketConn. transport, through its WriteTo
|
||||
// and ReadFrom methods, handles the actual sending and receiving the DNS
|
||||
// messages encoded by DNSPacketConn. addr is the address to be passed to
|
||||
// transport.WriteTo whenever a message needs to be sent.
|
||||
func NewDNSPacketConn(transport net.PacketConn, addr net.Addr, domain dns.Name) *DNSPacketConn {
|
||||
// Generate a new random ClientID.
|
||||
clientID := turbotunnel.NewClientID()
|
||||
c := &DNSPacketConn{
|
||||
clientID: clientID,
|
||||
domain: domain,
|
||||
pollChan: make(chan struct{}, pollLimit),
|
||||
QueuePacketConn: turbotunnel.NewQueuePacketConn(clientID, 0),
|
||||
}
|
||||
go func() {
|
||||
err := c.recvLoop(transport)
|
||||
if err != nil {
|
||||
log.Printf("recvLoop: %v", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
err := c.sendLoop(transport, addr)
|
||||
if err != nil {
|
||||
log.Printf("sendLoop: %v", err)
|
||||
}
|
||||
}()
|
||||
return c
|
||||
}
|
||||
|
||||
// dnsResponsePayload extracts the downstream payload of a DNS response, encoded
|
||||
// into the RDATA of a TXT RR. It returns nil if the message doesn't pass format
|
||||
// checks, or if the name in its Question entry is not a subdomain of domain.
|
||||
func dnsResponsePayload(resp *dns.Message, domain dns.Name) []byte {
|
||||
if resp.Flags&0x8000 != 0x8000 {
|
||||
// QR != 1, this is not a response.
|
||||
return nil
|
||||
}
|
||||
if resp.Flags&0x000f != dns.RcodeNoError {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(resp.Answer) != 1 {
|
||||
return nil
|
||||
}
|
||||
answer := resp.Answer[0]
|
||||
|
||||
_, ok := answer.Name.TrimSuffix(domain)
|
||||
if !ok {
|
||||
// Not the name we are expecting.
|
||||
return nil
|
||||
}
|
||||
|
||||
if answer.Type != dns.RRTypeTXT {
|
||||
// We only support TYPE == TXT.
|
||||
return nil
|
||||
}
|
||||
payload, err := dns.DecodeRDataTXT(answer.Data)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
// nextPacket reads the next length-prefixed packet from r. It returns a nil
|
||||
// error only when a complete packet was read. It returns io.EOF only when there
|
||||
// were 0 bytes remaining to read from r. It returns io.ErrUnexpectedEOF when
|
||||
// EOF occurs in the middle of an encoded packet.
|
||||
func nextPacket(r *bytes.Reader) ([]byte, error) {
|
||||
for {
|
||||
var n uint16
|
||||
err := binary.Read(r, binary.BigEndian, &n)
|
||||
if err != nil {
|
||||
// We may return a real io.EOF only here.
|
||||
return nil, err
|
||||
}
|
||||
p := make([]byte, n)
|
||||
_, err = io.ReadFull(r, p)
|
||||
// Here we must change io.EOF to io.ErrUnexpectedEOF.
|
||||
if err == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
}
|
||||
|
||||
// recvLoop repeatedly calls transport.ReadFrom to receive a DNS message,
|
||||
// extracts its payload and breaks it into packets, and stores the packets in a
|
||||
// queue to be returned from a future call to c.ReadFrom.
|
||||
//
|
||||
// Whenever we receive a DNS response containing at least one data packet, we
|
||||
// send on c.pollChan to permit sendLoop to send an immediate polling queries.
|
||||
// KCP itself will also send an ACK packet for incoming data, which is
|
||||
// effectively a second poll. Therefore, each time we receive data, we send up
|
||||
// to 2 polling queries (or 1 + f polling queries, if KCP only ACKs an f
|
||||
// fraction of incoming data). We say "up to" because sendLoop will discard an
|
||||
// empty polling query if it has an organic non-empty packet to send (this goes
|
||||
// also for KCP's organic ACK packets).
|
||||
//
|
||||
// The intuition behind polling immediately after receiving is that if server
|
||||
// has just had something to send, it may have more to send, and in order for
|
||||
// the server to send anything, we must give it a query to respond to. The
|
||||
// intuition behind polling *2 times* (or 1 + f times) is similar to TCP slow
|
||||
// start: we want to maintain some number of queries "in flight", and the faster
|
||||
// the server is sending, the higher that number should be. If we polled only
|
||||
// once for each received packet, we would tend to have only one query in flight
|
||||
// at a time, ping-pong style. The first polling query replaces the in-flight
|
||||
// query that has just finished its duty in returning data to us; the second
|
||||
// grows the effective in-flight window proportional to the rate at which
|
||||
// data-carrying responses are being received. Compare to Eq. (2) of
|
||||
// https://tools.ietf.org/html/rfc5681#section-3.1. The differences are that we
|
||||
// count messages, not bytes, and we don't maintain an explicit window. If a
|
||||
// response comes back without data, or if a query or response is dropped by the
|
||||
// network, then we don't poll again, which decreases the effective in-flight
|
||||
// window.
|
||||
func (c *DNSPacketConn) recvLoop(transport net.PacketConn) error {
|
||||
for {
|
||||
var buf [4096]byte
|
||||
n, addr, err := transport.ReadFrom(buf[:])
|
||||
if err != nil {
|
||||
if err, ok := err.(net.Error); ok && err.Temporary() {
|
||||
log.Printf("ReadFrom temporary error: %v", err)
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Got a response. Try to parse it as a DNS message.
|
||||
resp, err := dns.MessageFromWireFormat(buf[:n])
|
||||
if err != nil {
|
||||
log.Printf("MessageFromWireFormat: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
payload := dnsResponsePayload(&resp, c.domain)
|
||||
|
||||
// Pull out the packets contained in the payload.
|
||||
r := bytes.NewReader(payload)
|
||||
any := false
|
||||
for {
|
||||
p, err := nextPacket(r)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
any = true
|
||||
c.QueuePacketConn.QueueIncoming(p, addr)
|
||||
}
|
||||
|
||||
// If the payload contained one or more packets, permit sendLoop
|
||||
// to poll immediately. ACKs on received data will effectively
|
||||
// serve as another stream of polls whose rate is proportional
|
||||
// to the rate of incoming packets.
|
||||
if any {
|
||||
select {
|
||||
case c.pollChan <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// chunks breaks p into non-empty subslices of at most n bytes, greedily so that
|
||||
// only final subslice has length < n.
|
||||
func chunks(p []byte, n int) [][]byte {
|
||||
var result [][]byte
|
||||
for len(p) > 0 {
|
||||
sz := len(p)
|
||||
if sz > n {
|
||||
sz = n
|
||||
}
|
||||
result = append(result, p[:sz])
|
||||
p = p[sz:]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// send sends p as a single packet encoded into a DNS query, using
|
||||
// transport.WriteTo(query, addr). The length of p must be less than 224 bytes.
|
||||
//
|
||||
// Here is an example of how a packet is encoded into a DNS name, using
|
||||
//
|
||||
// p = "supercalifragilisticexpialidocious"
|
||||
// c.clientID = "CLIENTID"
|
||||
// domain = "t.example.com"
|
||||
//
|
||||
// as the input.
|
||||
//
|
||||
// 0. Start with the raw packet contents.
|
||||
//
|
||||
// supercalifragilisticexpialidocious
|
||||
//
|
||||
// 1. Length-prefix the packet and add random padding. A length prefix L < 0xe0
|
||||
// means a data packet of L bytes. A length prefix L ≥ 0xe0 means padding
|
||||
// of L − 0xe0 bytes (not counting the length of the length prefix itself).
|
||||
//
|
||||
// \xe3\xd9\xa3\x15\x22supercalifragilisticexpialidocious
|
||||
//
|
||||
// 2. Prefix the ClientID.
|
||||
//
|
||||
// CLIENTID\xe3\xd9\xa3\x15\x22supercalifragilisticexpialidocious
|
||||
//
|
||||
// 3. Base32-encode, without padding and in lower case.
|
||||
//
|
||||
// ingesrkokreujy6zumkse43vobsxey3bnruwm4tbm5uwy2ltoruwgzlyobuwc3djmrxwg2lpovzq
|
||||
//
|
||||
// 4. Break into labels of at most 63 octets.
|
||||
//
|
||||
// ingesrkokreujy6zumkse43vobsxey3bnruwm4tbm5uwy2ltoruwgzlyobuwc3d.jmrxwg2lpovzq
|
||||
//
|
||||
// 5. Append the domain.
|
||||
//
|
||||
// ingesrkokreujy6zumkse43vobsxey3bnruwm4tbm5uwy2ltoruwgzlyobuwc3d.jmrxwg2lpovzq.t.example.com
|
||||
func (c *DNSPacketConn) send(transport net.PacketConn, p []byte, addr net.Addr) error {
|
||||
var decoded []byte
|
||||
{
|
||||
if len(p) >= 224 {
|
||||
return fmt.Errorf("too long")
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
// ClientID
|
||||
buf.Write(c.clientID[:])
|
||||
n := numPadding
|
||||
if len(p) == 0 {
|
||||
n = numPaddingForPoll
|
||||
}
|
||||
// Padding / cache inhibition
|
||||
buf.WriteByte(byte(224 + n))
|
||||
io.CopyN(&buf, rand.Reader, int64(n))
|
||||
// Packet contents
|
||||
if len(p) > 0 {
|
||||
buf.WriteByte(byte(len(p)))
|
||||
buf.Write(p)
|
||||
}
|
||||
decoded = buf.Bytes()
|
||||
}
|
||||
|
||||
encoded := make([]byte, base32Encoding.EncodedLen(len(decoded)))
|
||||
base32Encoding.Encode(encoded, decoded)
|
||||
encoded = bytes.ToLower(encoded)
|
||||
labels := chunks(encoded, 63)
|
||||
labels = append(labels, c.domain...)
|
||||
name, err := dns.NewName(labels)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var id uint16
|
||||
binary.Read(rand.Reader, binary.BigEndian, &id)
|
||||
query := &dns.Message{
|
||||
ID: id,
|
||||
Flags: 0x0100, // QR = 0, RD = 1
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: name,
|
||||
Type: dns.RRTypeTXT,
|
||||
Class: dns.ClassIN,
|
||||
},
|
||||
},
|
||||
// EDNS(0)
|
||||
Additional: []dns.RR{
|
||||
{
|
||||
Name: dns.Name{},
|
||||
Type: dns.RRTypeOPT,
|
||||
Class: 4096, // requester's UDP payload size
|
||||
TTL: 0, // extended RCODE and flags
|
||||
Data: []byte{},
|
||||
},
|
||||
},
|
||||
}
|
||||
buf, err := query.WireFormat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = transport.WriteTo(buf, addr)
|
||||
return err
|
||||
}
|
||||
|
||||
// sendLoop takes packets that have been written using c.WriteTo, and sends them
|
||||
// on the network using send. It also does polling with empty packets when
|
||||
// requested by pollChan or after a timeout.
|
||||
func (c *DNSPacketConn) sendLoop(transport net.PacketConn, addr net.Addr) error {
|
||||
pollDelay := initPollDelay
|
||||
pollTimer := time.NewTimer(pollDelay)
|
||||
for {
|
||||
var p []byte
|
||||
outgoing := c.QueuePacketConn.OutgoingQueue(addr)
|
||||
pollTimerExpired := false
|
||||
// Prioritize sending an actual data packet from outgoing. Only
|
||||
// consider a poll when outgoing is empty.
|
||||
select {
|
||||
case p = <-outgoing:
|
||||
default:
|
||||
select {
|
||||
case p = <-outgoing:
|
||||
case <-c.pollChan:
|
||||
case <-pollTimer.C:
|
||||
pollTimerExpired = true
|
||||
}
|
||||
}
|
||||
|
||||
if len(p) > 0 {
|
||||
// A data-carrying packet displaces one pending poll
|
||||
// opportunity, if any.
|
||||
select {
|
||||
case <-c.pollChan:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
if pollTimerExpired {
|
||||
// We're polling because it's been a while since we last
|
||||
// polled. Increase the poll delay.
|
||||
pollDelay = time.Duration(float64(pollDelay) * pollDelayMultiplier)
|
||||
if pollDelay > maxPollDelay {
|
||||
pollDelay = maxPollDelay
|
||||
}
|
||||
} else {
|
||||
// We're sending an actual data packet, or we're polling
|
||||
// in response to a received packet. Reset the poll
|
||||
// delay to initial.
|
||||
if !pollTimer.Stop() {
|
||||
<-pollTimer.C
|
||||
}
|
||||
pollDelay = initPollDelay
|
||||
}
|
||||
pollTimer.Reset(pollDelay)
|
||||
|
||||
// Unlike in the server, in the client we assume that because
|
||||
// the data capacity of queries is so limited, it's not worth
|
||||
// trying to send more than one packet per query.
|
||||
err := c.send(transport, p, addr)
|
||||
if err != nil {
|
||||
log.Printf("send: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
176
internal/dnsttclient/http.go
Normal file
176
internal/dnsttclient/http.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package dnsttclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"socksrevivepc/internal/dnsttcore/turbotunnel"
|
||||
)
|
||||
|
||||
// A default Retry-After delay to use when there is no explicit Retry-After
|
||||
// header in an HTTP response.
|
||||
const defaultRetryAfter = 10 * time.Second
|
||||
|
||||
// HTTPPacketConn is an HTTP-based transport for DNS messages, used for DNS over
|
||||
// HTTPS (DoH). Its WriteTo and ReadFrom methods exchange DNS messages over HTTP
|
||||
// requests and responses.
|
||||
//
|
||||
// HTTPPacketConn deals only with already formatted DNS messages. It does not
|
||||
// handle encoding information into the messages. That is rather the
|
||||
// responsibility of DNSPacketConn.
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc8484
|
||||
type HTTPPacketConn struct {
|
||||
// client is the http.Client used to make requests. We use this instead
|
||||
// of http.DefaultClient in order to support setting a timeout and a
|
||||
// uTLS fingerprint.
|
||||
client *http.Client
|
||||
|
||||
// urlString is the URL to which HTTP requests will be sent, for example
|
||||
// "https://doh.example/dns-query".
|
||||
urlString string
|
||||
|
||||
// notBefore, if not zero, is a time before which we may not send any
|
||||
// queries; queries are buffered or dropped until that time. notBefore
|
||||
// is set when we get a 429 Too Many Requests HTTP response or other
|
||||
// unexpected status code that causes us to need to slow down. It is set
|
||||
// according to the Retry-After header if available, otherwise it is set
|
||||
// to defaultRetryAfter in the future. notBeforeLock controls access to
|
||||
// notBefore.
|
||||
notBefore time.Time
|
||||
notBeforeLock sync.RWMutex
|
||||
|
||||
// QueuePacketConn is the direct receiver of ReadFrom and WriteTo calls.
|
||||
// sendLoop, via send, removes messages from the outgoing queue that
|
||||
// were placed there by WriteTo, and inserts messages into the incoming
|
||||
// queue to be returned from ReadFrom.
|
||||
*turbotunnel.QueuePacketConn
|
||||
}
|
||||
|
||||
// NewHTTPPacketConn creates a new HTTPPacketConn configured to use the HTTP
|
||||
// server at urlString as a DNS over HTTP resolver. client is the http.Client
|
||||
// that will be used to make requests. urlString should include any necessary
|
||||
// path components; e.g., "/dns-query". numSenders is the number of concurrent
|
||||
// sender-receiver goroutines to run.
|
||||
func NewHTTPPacketConn(rt http.RoundTripper, urlString string, numSenders int) (*HTTPPacketConn, error) {
|
||||
c := &HTTPPacketConn{
|
||||
client: &http.Client{
|
||||
Transport: rt,
|
||||
Timeout: 1 * time.Minute,
|
||||
},
|
||||
urlString: urlString,
|
||||
QueuePacketConn: turbotunnel.NewQueuePacketConn(turbotunnel.DummyAddr{}, 0),
|
||||
}
|
||||
for i := 0; i < numSenders; i++ {
|
||||
go c.sendLoop()
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// send sends a message in an HTTP request, and queues the body HTTP response to
|
||||
// be returned from a future call to ReadFrom.
|
||||
func (c *HTTPPacketConn) send(p []byte) error {
|
||||
req, err := http.NewRequest("POST", c.urlString, bytes.NewReader(p))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Accept", "application/dns-message")
|
||||
req.Header.Set("Content-Type", "application/dns-message")
|
||||
req.Header.Set("User-Agent", "") // Disable default "Go-http-client/1.1".
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
if ct := resp.Header.Get("Content-Type"); ct != "application/dns-message" {
|
||||
return fmt.Errorf("unknown HTTP response Content-Type %+q", ct)
|
||||
}
|
||||
body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 64000))
|
||||
if err == nil {
|
||||
c.QueuePacketConn.QueueIncoming(body, turbotunnel.DummyAddr{})
|
||||
}
|
||||
// Ignore err != nil; don't report an error if we at least
|
||||
// managed to send.
|
||||
default:
|
||||
// We primarily are thinking of 429 Too Many Requests here, but
|
||||
// any other unexpected response codes will also cause us to
|
||||
// rate-limit ourselves and emit a log message.
|
||||
// https://developers.google.com/speed/public-dns/docs/doh/#errors
|
||||
now := time.Now()
|
||||
var retryAfter time.Time
|
||||
if value := resp.Header.Get("Retry-After"); value != "" {
|
||||
var err error
|
||||
retryAfter, err = parseRetryAfter(value, now)
|
||||
if err != nil {
|
||||
log.Printf("cannot parse Retry-After value %+q", value)
|
||||
}
|
||||
}
|
||||
if retryAfter.IsZero() {
|
||||
// Supply a default.
|
||||
retryAfter = now.Add(defaultRetryAfter)
|
||||
}
|
||||
if retryAfter.Before(now) {
|
||||
log.Printf("got %+q, but Retry-After is %v in the past",
|
||||
resp.Status, now.Sub(retryAfter))
|
||||
} else {
|
||||
c.notBeforeLock.Lock()
|
||||
if retryAfter.Before(c.notBefore) {
|
||||
log.Printf("got %+q, but Retry-After is %v earlier than already received Retry-After",
|
||||
resp.Status, c.notBefore.Sub(retryAfter))
|
||||
} else {
|
||||
log.Printf("got %+q; ceasing sending for %v",
|
||||
resp.Status, retryAfter.Sub(now))
|
||||
c.notBefore = retryAfter
|
||||
}
|
||||
c.notBeforeLock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendLoop loops over the contents of the outgoing queue and passes them to
|
||||
// send. It drops packets while c.notBefore is in the future.
|
||||
func (c *HTTPPacketConn) sendLoop() {
|
||||
for p := range c.QueuePacketConn.OutgoingQueue(turbotunnel.DummyAddr{}) {
|
||||
// Stop sending while we are rate-limiting ourselves (as a
|
||||
// result of a Retry-After response header, for example).
|
||||
c.notBeforeLock.RLock()
|
||||
notBefore := c.notBefore
|
||||
c.notBeforeLock.RUnlock()
|
||||
if wait := notBefore.Sub(time.Now()); wait > 0 {
|
||||
// Drop it.
|
||||
continue
|
||||
}
|
||||
|
||||
err := c.send(p)
|
||||
if err != nil {
|
||||
log.Printf("sendLoop: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseRetryAfter parses the value of a Retry-After header as an absolute
|
||||
// time.Time.
|
||||
func parseRetryAfter(value string, now time.Time) (time.Time, error) {
|
||||
// May be a date string or an integer number of seconds.
|
||||
// https://tools.ietf.org/html/rfc7231#section-7.1.3
|
||||
if t, err := http.ParseTime(value); err == nil {
|
||||
return t, nil
|
||||
}
|
||||
i, err := strconv.ParseUint(value, 10, 32)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return now.Add(time.Duration(i) * time.Second), nil
|
||||
}
|
||||
445
internal/dnsttclient/main.go
Normal file
445
internal/dnsttclient/main.go
Normal file
@@ -0,0 +1,445 @@
|
||||
// dnstt-client is the client end of a DNS tunnel.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// dnstt-client [-doh URL|-dot ADDR|-udp ADDR] -pubkey-file PUBKEYFILE DOMAIN LOCALADDR
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// dnstt-client -doh https://resolver.example/dns-query -pubkey-file server.pub t.example.com 127.0.0.1:7000
|
||||
// dnstt-client -dot resolver.example:853 -pubkey-file server.pub t.example.com 127.0.0.1:7000
|
||||
//
|
||||
// The program supports DNS over HTTPS (DoH), DNS over TLS (DoT), and UDP DNS.
|
||||
// Use one of these options:
|
||||
//
|
||||
// -doh https://resolver.example/dns-query
|
||||
// -dot resolver.example:853
|
||||
// -udp resolver.example:53
|
||||
//
|
||||
// You can give the server's public key as a file or as a hex string. Use
|
||||
// "dnstt-server -gen-key" to get the public key.
|
||||
//
|
||||
// -pubkey-file server.pub
|
||||
// -pubkey 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff
|
||||
//
|
||||
// DOMAIN is the root of the DNS zone reserved for the tunnel. See README for
|
||||
// instructions on setting it up.
|
||||
//
|
||||
// LOCALADDR is the TCP address that will listen for connections and forward
|
||||
// them over the tunnel.
|
||||
//
|
||||
// In -doh and -dot modes, the program's TLS fingerprint is camouflaged with
|
||||
// uTLS by default. The specific TLS fingerprint is selected randomly from a
|
||||
// weighted distribution. You can set your own distribution (or specific single
|
||||
// fingerprint) using the -utls option. The special value "none" disables uTLS.
|
||||
//
|
||||
// -utls '3*Firefox,2*Chrome,1*iOS'
|
||||
// -utls Firefox
|
||||
// -utls none
|
||||
package dnsttclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
utls "github.com/refraction-networking/utls"
|
||||
"github.com/xtaci/kcp-go/v5"
|
||||
"github.com/xtaci/smux"
|
||||
"socksrevivepc/internal/dnsttcore/dns"
|
||||
"socksrevivepc/internal/dnsttcore/noise"
|
||||
"socksrevivepc/internal/dnsttcore/turbotunnel"
|
||||
)
|
||||
|
||||
// smux streams will be closed after this much time without receiving data.
|
||||
const idleTimeout = 2 * time.Minute
|
||||
|
||||
// dnsNameCapacity returns the number of bytes remaining for encoded data after
|
||||
// including domain in a DNS name.
|
||||
func dnsNameCapacity(domain dns.Name) int {
|
||||
// Names must be 255 octets or shorter in total length.
|
||||
// https://tools.ietf.org/html/rfc1035#section-2.3.4
|
||||
capacity := 255
|
||||
// Subtract the length of the null terminator.
|
||||
capacity -= 1
|
||||
for _, label := range domain {
|
||||
// Subtract the length of the label and the length octet.
|
||||
capacity -= len(label) + 1
|
||||
}
|
||||
// Each label may be up to 63 bytes long and requires 64 bytes to
|
||||
// encode.
|
||||
capacity = capacity * 63 / 64
|
||||
// Base32 expands every 5 bytes to 8.
|
||||
capacity = capacity * 5 / 8
|
||||
return capacity
|
||||
}
|
||||
|
||||
// readKeyFromFile reads a key from a named file.
|
||||
func readKeyFromFile(filename string) ([]byte, error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
return noise.ReadKey(f)
|
||||
}
|
||||
|
||||
// sampleUTLSDistribution parses a weighted uTLS Client Hello ID distribution
|
||||
// string of the form "3*Firefox,2*Chrome,1*iOS", matches each label to a
|
||||
// utls.ClientHelloID from utlsClientHelloIDMap, and randomly samples one
|
||||
// utls.ClientHelloID from the distribution.
|
||||
func sampleUTLSDistribution(spec string) (*utls.ClientHelloID, error) {
|
||||
weights, labels, err := parseWeightedList(spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := make([]*utls.ClientHelloID, 0, len(labels))
|
||||
for _, label := range labels {
|
||||
var id *utls.ClientHelloID
|
||||
if label == "none" {
|
||||
id = nil
|
||||
} else {
|
||||
id = utlsLookup(label)
|
||||
if id == nil {
|
||||
return nil, fmt.Errorf("unknown TLS fingerprint %q", label)
|
||||
}
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids[sampleWeighted(weights)], nil
|
||||
}
|
||||
|
||||
func handle(local *net.TCPConn, sess *smux.Session, conv uint32) error {
|
||||
stream, err := sess.OpenStream()
|
||||
if err != nil {
|
||||
return fmt.Errorf("session %08x opening stream: %v", conv, err)
|
||||
}
|
||||
defer func() {
|
||||
log.Printf("end stream %08x:%d", conv, stream.ID())
|
||||
stream.Close()
|
||||
}()
|
||||
log.Printf("begin stream %08x:%d", conv, stream.ID())
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_, err := io.Copy(stream, local)
|
||||
if err == io.EOF {
|
||||
// smux Stream.Write may return io.EOF.
|
||||
err = nil
|
||||
}
|
||||
if err != nil && !errors.Is(err, io.ErrClosedPipe) {
|
||||
log.Printf("stream %08x:%d copy stream←local: %v", conv, stream.ID(), err)
|
||||
}
|
||||
local.CloseRead()
|
||||
stream.Close()
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_, err := io.Copy(local, stream)
|
||||
if err == io.EOF {
|
||||
// smux Stream.WriteTo may return io.EOF.
|
||||
err = nil
|
||||
}
|
||||
if err != nil && !errors.Is(err, io.ErrClosedPipe) {
|
||||
log.Printf("stream %08x:%d copy local←stream: %v", conv, stream.ID(), err)
|
||||
}
|
||||
local.CloseWrite()
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func run(pubkey []byte, domain dns.Name, localAddr *net.TCPAddr, remoteAddr net.Addr, pconn net.PacketConn) error {
|
||||
defer pconn.Close()
|
||||
|
||||
ln, err := net.ListenTCP("tcp", localAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening local listener: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
mtu := dnsNameCapacity(domain) - 8 - 1 - numPadding - 1 // clientid + padding length prefix + padding + data length prefix
|
||||
if mtu < 80 {
|
||||
return fmt.Errorf("domain %s leaves only %d bytes for payload", domain, mtu)
|
||||
}
|
||||
log.Printf("effective MTU %d", mtu)
|
||||
|
||||
// Open a KCP conn on the PacketConn.
|
||||
conn, err := kcp.NewConn2(remoteAddr, nil, 0, 0, pconn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening KCP conn: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
log.Printf("end session %08x", conn.GetConv())
|
||||
conn.Close()
|
||||
}()
|
||||
log.Printf("begin session %08x", conn.GetConv())
|
||||
// Permit coalescing the payloads of consecutive sends.
|
||||
conn.SetStreamMode(true)
|
||||
// Disable the dynamic congestion window (limit only by the maximum of
|
||||
// local and remote static windows).
|
||||
conn.SetNoDelay(
|
||||
0, // default nodelay
|
||||
0, // default interval
|
||||
0, // default resend
|
||||
1, // nc=1 => congestion window off
|
||||
)
|
||||
conn.SetWindowSize(turbotunnel.QueueSize/2, turbotunnel.QueueSize/2)
|
||||
if rc := conn.SetMtu(mtu); !rc {
|
||||
panic(rc)
|
||||
}
|
||||
|
||||
// Put a Noise channel on top of the KCP conn.
|
||||
rw, err := noise.NewClient(conn, pubkey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start a smux session on the Noise channel.
|
||||
smuxConfig := smux.DefaultConfig()
|
||||
smuxConfig.Version = 2
|
||||
smuxConfig.KeepAliveTimeout = idleTimeout
|
||||
smuxConfig.MaxStreamBuffer = 1 * 1024 * 1024 // default is 65536
|
||||
sess, err := smux.Client(rw, smuxConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening smux session: %v", err)
|
||||
}
|
||||
defer sess.Close()
|
||||
|
||||
for {
|
||||
local, err := ln.Accept()
|
||||
if err != nil {
|
||||
if err, ok := err.(net.Error); ok && err.Temporary() {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
defer local.Close()
|
||||
err := handle(local.(*net.TCPConn), sess, conn.GetConv())
|
||||
if err != nil {
|
||||
log.Printf("handle: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
var dohURL string
|
||||
var dotAddr string
|
||||
var pubkeyFilename string
|
||||
var pubkeyString string
|
||||
var udpAddr string
|
||||
var utlsDistribution string
|
||||
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(flag.CommandLine.Output(), `Usage:
|
||||
%[1]s [-doh URL|-dot ADDR|-udp ADDR] -pubkey-file PUBKEYFILE DOMAIN LOCALADDR
|
||||
|
||||
Examples:
|
||||
%[1]s -doh https://resolver.example/dns-query -pubkey-file server.pub t.example.com 127.0.0.1:7000
|
||||
%[1]s -dot resolver.example:853 -pubkey-file server.pub t.example.com 127.0.0.1:7000
|
||||
|
||||
`, os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
labels := make([]string, 0, len(utlsClientHelloIDMap))
|
||||
labels = append(labels, "none")
|
||||
for _, entry := range utlsClientHelloIDMap {
|
||||
labels = append(labels, entry.Label)
|
||||
}
|
||||
fmt.Fprintf(flag.CommandLine.Output(), `
|
||||
Known TLS fingerprints for -utls are:
|
||||
`)
|
||||
i := 0
|
||||
for i < len(labels) {
|
||||
var line strings.Builder
|
||||
fmt.Fprintf(&line, " %s", labels[i])
|
||||
w := 2 + len(labels[i])
|
||||
i++
|
||||
for i < len(labels) && w+1+len(labels[i]) <= 72 {
|
||||
fmt.Fprintf(&line, " %s", labels[i])
|
||||
w += 1 + len(labels[i])
|
||||
i++
|
||||
}
|
||||
fmt.Fprintln(flag.CommandLine.Output(), line.String())
|
||||
}
|
||||
}
|
||||
flag.StringVar(&dohURL, "doh", "", "URL of DoH resolver")
|
||||
flag.StringVar(&dotAddr, "dot", "", "address of DoT resolver")
|
||||
flag.StringVar(&pubkeyString, "pubkey", "", fmt.Sprintf("server public key (%d hex digits)", noise.KeyLen*2))
|
||||
flag.StringVar(&pubkeyFilename, "pubkey-file", "", "read server public key from file")
|
||||
flag.StringVar(&udpAddr, "udp", "", "address of UDP DNS resolver")
|
||||
flag.StringVar(&utlsDistribution, "utls",
|
||||
"4*random,3*Firefox_120,1*Firefox_105,3*Chrome_120,1*Chrome_102,1*iOS_14,1*iOS_13",
|
||||
"choose TLS fingerprint from weighted distribution")
|
||||
flag.Parse()
|
||||
|
||||
log.SetFlags(log.LstdFlags | log.LUTC)
|
||||
|
||||
if flag.NArg() != 2 {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
domain, err := dns.ParseName(flag.Arg(0))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "invalid domain %+q: %v\n", flag.Arg(0), err)
|
||||
os.Exit(1)
|
||||
}
|
||||
localAddr, err := net.ResolveTCPAddr("tcp", flag.Arg(1))
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var pubkey []byte
|
||||
if pubkeyFilename != "" && pubkeyString != "" {
|
||||
fmt.Fprintf(os.Stderr, "only one of -pubkey and -pubkey-file may be used\n")
|
||||
os.Exit(1)
|
||||
} else if pubkeyFilename != "" {
|
||||
var err error
|
||||
pubkey, err = readKeyFromFile(pubkeyFilename)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "cannot read pubkey from file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else if pubkeyString != "" {
|
||||
var err error
|
||||
pubkey, err = noise.DecodeKey(pubkeyString)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "pubkey format error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
if len(pubkey) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "the -pubkey or -pubkey-file option is required\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
tlsConfig, err := loadTLSConfig()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "TLS config error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
utlsClientHelloID, err := sampleUTLSDistribution(utlsDistribution)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "parsing -utls: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if utlsClientHelloID != nil {
|
||||
log.Printf("uTLS fingerprint %s %s", utlsClientHelloID.Client, utlsClientHelloID.Version)
|
||||
}
|
||||
|
||||
// Iterate over the remote resolver address options and select one and
|
||||
// only one.
|
||||
var remoteAddr net.Addr
|
||||
var pconn net.PacketConn
|
||||
for _, opt := range []struct {
|
||||
s string
|
||||
f func(string) (net.Addr, net.PacketConn, error)
|
||||
}{
|
||||
// -doh
|
||||
{dohURL, func(s string) (net.Addr, net.PacketConn, error) {
|
||||
addr := turbotunnel.DummyAddr{}
|
||||
var rt http.RoundTripper
|
||||
if utlsClientHelloID == nil {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
// Disable DefaultTransport's default Proxy =
|
||||
// ProxyFromEnvironment setting, for conformity
|
||||
// with utlsRoundTripper and with DoT mode,
|
||||
// which do not take a proxy from the
|
||||
// environment.
|
||||
transport.Proxy = nil
|
||||
transport.TLSClientConfig = tlsConfig.Clone()
|
||||
baseDialContext := transport.DialContext
|
||||
if baseDialContext == nil {
|
||||
baseDialContext = (&net.Dialer{}).DialContext
|
||||
}
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
if network == "tcp" {
|
||||
resolvedAddr, _, err := resolveAddrIPv4(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addr = resolvedAddr
|
||||
network = "tcp4"
|
||||
}
|
||||
return baseDialContext(ctx, network, addr)
|
||||
}
|
||||
rt = transport
|
||||
} else {
|
||||
utlsConfig := &utls.Config{
|
||||
RootCAs: tlsConfig.RootCAs,
|
||||
MinVersion: tlsConfig.MinVersion,
|
||||
}
|
||||
rt = NewUTLSRoundTripper(utlsConfig, utlsClientHelloID, true)
|
||||
}
|
||||
pconn, err := NewHTTPPacketConn(rt, dohURL, 32)
|
||||
return addr, pconn, err
|
||||
}},
|
||||
// -dot
|
||||
{dotAddr, func(s string) (net.Addr, net.PacketConn, error) {
|
||||
addr := turbotunnel.DummyAddr{}
|
||||
var dialTLSContext func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
if utlsClientHelloID == nil {
|
||||
dialTLSContext = (&tls.Dialer{Config: tlsConfig.Clone()}).DialContext
|
||||
} else {
|
||||
utlsConfig := &utls.Config{
|
||||
RootCAs: tlsConfig.RootCAs,
|
||||
MinVersion: tlsConfig.MinVersion,
|
||||
}
|
||||
dialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return utlsDialContext(ctx, network, addr, utlsConfig, utlsClientHelloID)
|
||||
}
|
||||
}
|
||||
pconn, err := NewTLSPacketConn(dotAddr, dialTLSContext)
|
||||
return addr, pconn, err
|
||||
}},
|
||||
// -udp
|
||||
{udpAddr, func(s string) (net.Addr, net.PacketConn, error) {
|
||||
addr, err := net.ResolveUDPAddr("udp", s)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
pconn, err := net.ListenUDP("udp", nil)
|
||||
return addr, pconn, err
|
||||
}},
|
||||
} {
|
||||
if opt.s == "" {
|
||||
continue
|
||||
}
|
||||
if pconn != nil {
|
||||
fmt.Fprintf(os.Stderr, "only one of -doh, -dot, and -udp may be given\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
var err error
|
||||
remoteAddr, pconn, err = opt.f(opt.s)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
if pconn == nil {
|
||||
fmt.Fprintf(os.Stderr, "one of -doh, -dot, or -udp is required\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
pconn = NewDNSPacketConn(pconn, remoteAddr, domain)
|
||||
err = run(pubkey, domain, localAddr, remoteAddr, pconn)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
134
internal/dnsttclient/tls.go
Normal file
134
internal/dnsttclient/tls.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package dnsttclient
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"socksrevivepc/internal/dnsttcore/turbotunnel"
|
||||
)
|
||||
|
||||
const dialTimeout = 30 * time.Second
|
||||
|
||||
// TLSPacketConn is a TLS- and TCP-based transport for DNS messages, used for
|
||||
// DNS over TLS (DoT). Its WriteTo and ReadFrom methods exchange DNS messages
|
||||
// over a TLS channel, prefixing each message with a two-octet length field as
|
||||
// in DNS over TCP.
|
||||
//
|
||||
// TLSPacketConn deals only with already formatted DNS messages. It does not
|
||||
// handle encoding information into the messages. That is rather the
|
||||
// responsibility of DNSPacketConn.
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc7858
|
||||
type TLSPacketConn struct {
|
||||
// QueuePacketConn is the direct receiver of ReadFrom and WriteTo calls.
|
||||
// recvLoop and sendLoop take the messages out of the receive and send
|
||||
// queues and actually put them on the network.
|
||||
*turbotunnel.QueuePacketConn
|
||||
}
|
||||
|
||||
// NewTLSPacketConn creates a new TLSPacketConn configured to use the TLS
|
||||
// server at addr as a DNS over TLS resolver. It maintains a TLS connection to
|
||||
// the resolver, reconnecting as necessary. It closes the connection if any
|
||||
// reconnection attempt fails.
|
||||
func NewTLSPacketConn(addr string, dialTLSContext func(ctx context.Context, network, addr string) (net.Conn, error)) (*TLSPacketConn, error) {
|
||||
dial := func() (net.Conn, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dialTimeout)
|
||||
defer cancel()
|
||||
return dialTLSContext(ctx, "tcp", addr)
|
||||
}
|
||||
// We maintain one TLS connection at a time, redialing it whenever it
|
||||
// becomes disconnected. We do the first dial here, outside the
|
||||
// goroutine, so that any immediate and permanent connection errors are
|
||||
// reported directly to the caller of NewTLSPacketConn.
|
||||
conn, err := dial()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &TLSPacketConn{
|
||||
QueuePacketConn: turbotunnel.NewQueuePacketConn(turbotunnel.DummyAddr{}, 0),
|
||||
}
|
||||
go func() {
|
||||
defer c.Close()
|
||||
for {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
err := c.recvLoop(conn)
|
||||
if err != nil {
|
||||
log.Printf("recvLoop: %v", err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
err := c.sendLoop(conn)
|
||||
if err != nil {
|
||||
log.Printf("sendLoop: %v", err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
wg.Wait()
|
||||
conn.Close()
|
||||
|
||||
// Whenever the TLS connection dies, redial a new one.
|
||||
conn, err = dial()
|
||||
if err != nil {
|
||||
log.Printf("dial tls: %v", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// recvLoop reads length-prefixed messages from conn and passes them to the
|
||||
// incoming queue.
|
||||
func (c *TLSPacketConn) recvLoop(conn net.Conn) error {
|
||||
br := bufio.NewReader(conn)
|
||||
for {
|
||||
var length uint16
|
||||
err := binary.Read(br, binary.BigEndian, &length)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
p := make([]byte, int(length))
|
||||
_, err = io.ReadFull(br, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.QueuePacketConn.QueueIncoming(p, turbotunnel.DummyAddr{})
|
||||
}
|
||||
}
|
||||
|
||||
// sendLoop reads messages from the outgoing queue and writes them,
|
||||
// length-prefixed, to conn.
|
||||
func (c *TLSPacketConn) sendLoop(conn net.Conn) error {
|
||||
bw := bufio.NewWriter(conn)
|
||||
for p := range c.QueuePacketConn.OutgoingQueue(turbotunnel.DummyAddr{}) {
|
||||
length := uint16(len(p))
|
||||
if int(length) != len(p) {
|
||||
panic(len(p))
|
||||
}
|
||||
err := binary.Write(bw, binary.BigEndian, &length)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = bw.Write(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = bw.Flush()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
337
internal/dnsttclient/utls.go
Normal file
337
internal/dnsttclient/utls.go
Normal file
@@ -0,0 +1,337 @@
|
||||
package dnsttclient
|
||||
|
||||
// Support code for TLS camouflage using uTLS.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
utls "github.com/refraction-networking/utls"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
// utlsClientHelloIDMap is a correspondence between human-readable labels and
|
||||
// supported utls.ClientHelloIDs.
|
||||
var utlsClientHelloIDMap = []struct {
|
||||
Label string
|
||||
ID *utls.ClientHelloID
|
||||
}{
|
||||
{"random", &utls.HelloRandomizedALPN},
|
||||
{"Firefox", &utls.HelloFirefox_Auto},
|
||||
{"Firefox_55", &utls.HelloFirefox_55},
|
||||
{"Firefox_56", &utls.HelloFirefox_56},
|
||||
{"Firefox_63", &utls.HelloFirefox_63},
|
||||
{"Firefox_65", &utls.HelloFirefox_65},
|
||||
{"Firefox_99", &utls.HelloFirefox_99},
|
||||
{"Firefox_102", &utls.HelloFirefox_102},
|
||||
{"Firefox_105", &utls.HelloFirefox_105},
|
||||
{"Firefox_120", &utls.HelloFirefox_120},
|
||||
{"Chrome", &utls.HelloChrome_Auto},
|
||||
{"Chrome_58", &utls.HelloChrome_58},
|
||||
{"Chrome_62", &utls.HelloChrome_62},
|
||||
{"Chrome_70", &utls.HelloChrome_70},
|
||||
{"Chrome_72", &utls.HelloChrome_72},
|
||||
{"Chrome_83", &utls.HelloChrome_83},
|
||||
{"Chrome_87", &utls.HelloChrome_87},
|
||||
{"Chrome_96", &utls.HelloChrome_96},
|
||||
{"Chrome_100", &utls.HelloChrome_100},
|
||||
{"Chrome_102", &utls.HelloChrome_102},
|
||||
{"Chrome_120", &utls.HelloChrome_120},
|
||||
{"iOS", &utls.HelloIOS_Auto},
|
||||
{"iOS_11_1", &utls.HelloIOS_11_1},
|
||||
{"iOS_12_1", &utls.HelloIOS_12_1},
|
||||
{"iOS_13", &utls.HelloIOS_13},
|
||||
{"iOS_14", &utls.HelloIOS_14},
|
||||
}
|
||||
|
||||
// utlsLookup returns a *utls.ClientHelloID from utlsClientHelloIDMap by a
|
||||
// case-insensitive label match, or nil if there is no match.
|
||||
func utlsLookup(label string) *utls.ClientHelloID {
|
||||
for _, entry := range utlsClientHelloIDMap {
|
||||
if strings.ToLower(label) == strings.ToLower(entry.Label) {
|
||||
return entry.ID
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var bootstrapResolverAddrs = []string{
|
||||
"8.8.8.8:53",
|
||||
"1.1.1.1:53",
|
||||
"8.8.4.4:53",
|
||||
"1.0.0.1:53",
|
||||
}
|
||||
|
||||
func lookupHostIPv4(ctx context.Context, host string) ([]net.IP, error) {
|
||||
var lastErr error
|
||||
for _, resolverAddr := range bootstrapResolverAddrs {
|
||||
resolverAddr := resolverAddr
|
||||
r := &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
d := &net.Dialer{Timeout: 5 * time.Second}
|
||||
return d.DialContext(ctx, "udp4", resolverAddr)
|
||||
},
|
||||
}
|
||||
ips, err := r.LookupIP(ctx, "ip4", host)
|
||||
if err == nil && len(ips) > 0 {
|
||||
return ips, nil
|
||||
}
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
if lastErr == nil {
|
||||
lastErr = fmt.Errorf("no A records returned")
|
||||
}
|
||||
return nil, fmt.Errorf("bootstrap IPv4 lookup failed for %s: %w", host, lastErr)
|
||||
}
|
||||
|
||||
func resolveAddrIPv4(ctx context.Context, addr string) (string, string, error) {
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
ip4 := ip.To4()
|
||||
if ip4 == nil {
|
||||
return "", "", fmt.Errorf("IPv6 address %s cannot be used for forced IPv4 dial", host)
|
||||
}
|
||||
return net.JoinHostPort(ip4.String(), port), host, nil
|
||||
}
|
||||
ips, err := lookupHostIPv4(ctx, host)
|
||||
if err != nil {
|
||||
return "", host, err
|
||||
}
|
||||
return net.JoinHostPort(ips[0].String(), port), host, nil
|
||||
}
|
||||
|
||||
// utlsDialContext connects to the given network address and initiates a TLS
|
||||
// handshake with the provided ClientHelloID, and returns the resulting TLS
|
||||
// connection.
|
||||
func utlsDialContext(ctx context.Context, network, addr string, config *utls.Config, id *utls.ClientHelloID) (*utls.UConn, error) {
|
||||
return utlsDialContextWithOptions(ctx, network, addr, config, id, false)
|
||||
}
|
||||
|
||||
func utlsDialContextWithOptions(ctx context.Context, network, addr string, config *utls.Config, id *utls.ClientHelloID, forceIPv4 bool) (*utls.UConn, error) {
|
||||
originalHost, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if forceIPv4 {
|
||||
addr, _, err = resolveAddrIPv4(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Set the SNI from the original addr host, if not already set.
|
||||
if config == nil {
|
||||
config = &utls.Config{}
|
||||
}
|
||||
if config.ServerName == "" {
|
||||
config = config.Clone()
|
||||
config.ServerName = originalHost
|
||||
}
|
||||
dialer := &net.Dialer{}
|
||||
if forceIPv4 && network == "tcp" {
|
||||
network = "tcp4"
|
||||
}
|
||||
conn, err := dialer.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uconn := utls.UClient(conn, config, *id)
|
||||
// We must call Handshake before returning, or else the UConn may not
|
||||
// actually use the selected ClientHelloID. It depends on whether a Read
|
||||
// or a Write happens first. If a Read happens first, the connection
|
||||
// will use the normal crypto/tls fingerprint. If a Write happens first,
|
||||
// it will use the selected fingerprint as expected.
|
||||
// https://github.com/refraction-networking/utls/issues/75
|
||||
err = uconn.Handshake()
|
||||
if err != nil {
|
||||
uconn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return uconn, nil
|
||||
}
|
||||
|
||||
// The goal of utlsRoundTripper is: provide an http.RoundTripper abstraction
|
||||
// that retains the features of http.Transport (e.g., persistent connections and
|
||||
// HTTP/2 support), while making TLS connections using uTLS in place of
|
||||
// crypto/tls. The challenge is: while http.Transport provides a DialTLSContext
|
||||
// hook, setting it to non-nil disables automatic HTTP/2 support in the client.
|
||||
// Most of the uTLS fingerprints contain an ALPN extension containing "h2";
|
||||
// i.e., they declare support for HTTP/2. If the server also supports HTTP/2,
|
||||
// then uTLS may negotiate an HTTP/2 connection without the http.Transport
|
||||
// knowing it, which leads to an HTTP/1.1 client speaking to an HTTP/2 server, a
|
||||
// protocol error.
|
||||
//
|
||||
// The code here uses an idea adapted from meek_lite in obfs4proxy:
|
||||
// https://gitlab.com/yawning/obfs4/commit/4d453dab2120082b00bf6e63ab4aaeeda6b8d8a3
|
||||
// Instead of setting DialTLSContext on an http.Transport and exposing it
|
||||
// directly, we expose a wrapper type, utlsRoundTripper, which contains within
|
||||
// it either an http.Transport or an http2.Transport. The first time a caller
|
||||
// calls RoundTrip on the wrapper, we initiate a uTLS connection
|
||||
// (bootstrapConn), then peek at the ALPN-negotiated protocol: if "h2", create
|
||||
// an internal http2.Transport; otherwise, create an internal http.Transport. In
|
||||
// either case, set DialTLSContext (or DialTLS for http2.Transport) on the
|
||||
// created Transport to a function that dials using uTLS. As a special case, the
|
||||
// first time the DialTLS callback is called, it reuses bootstrapConn (the one
|
||||
// made to peek at the ALPN), rather than make a new connection.
|
||||
//
|
||||
// Subsequent calls to RoundTripper on the wrapper just pass the requests though
|
||||
// the previously created http.Transport or http2.Transport. We assume that in
|
||||
// future RoundTrips, the ALPN-negotiated protocol will remain the same as it
|
||||
// was in the initial RoundTrip. At this point it is the http.Transport or
|
||||
// http2.Transport calling DialTLSContext, not us, so we cannot dynamically swap
|
||||
// the underlying transport based on the ALPN.
|
||||
//
|
||||
// https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/meek/29077
|
||||
// https://github.com/refraction-networking/utls/issues/16
|
||||
|
||||
// utlsRoundTripper is an http.RoundTripper that uses uTLS (with a specified
|
||||
// ClientHelloID) to make TLS connections.
|
||||
//
|
||||
// Can only be reused among servers which negotiate the same ALPN.
|
||||
type utlsRoundTripper struct {
|
||||
clientHelloID *utls.ClientHelloID
|
||||
config *utls.Config
|
||||
forceIPv4 bool
|
||||
innerLock sync.Mutex
|
||||
inner http.RoundTripper
|
||||
}
|
||||
|
||||
// NewUTLSRoundTripper creates a utlsRoundTripper with the given TLS
|
||||
// configuration and ClientHelloID.
|
||||
func NewUTLSRoundTripper(config *utls.Config, id *utls.ClientHelloID, forceIPv4 bool) *utlsRoundTripper {
|
||||
return &utlsRoundTripper{
|
||||
clientHelloID: id,
|
||||
config: config,
|
||||
forceIPv4: forceIPv4,
|
||||
// inner will be set in the first call to RoundTrip.
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
switch req.URL.Scheme {
|
||||
case "http":
|
||||
// If http, don't invoke uTLS; just pass it to an ordinary http.Transport.
|
||||
return http.DefaultTransport.RoundTrip(req)
|
||||
case "https":
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported URL scheme %q", req.URL.Scheme)
|
||||
}
|
||||
|
||||
var err error
|
||||
rt.innerLock.Lock()
|
||||
if rt.inner == nil {
|
||||
// On the first call, make an http.Transport or http2.Transport
|
||||
// as appropriate.
|
||||
rt.inner, err = makeRoundTripper(req, rt.config, rt.clientHelloID, rt.forceIPv4)
|
||||
}
|
||||
rt.innerLock.Unlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Forward the request to the inner http.Transport or http2.Transport.
|
||||
return rt.inner.RoundTrip(req)
|
||||
}
|
||||
|
||||
// makeRoundTripper makes a bootstrap TLS configuration using the given TLS
|
||||
// configuration and ClientHelloID, and creates an http.Transport or
|
||||
// http2.Transport, depending on the negotated ALPN. The Transport is set up to
|
||||
// make future TLS connections using the same TLS configuration and
|
||||
// ClientHelloID.
|
||||
func makeRoundTripper(req *http.Request, config *utls.Config, id *utls.ClientHelloID, forceIPv4 bool) (http.RoundTripper, error) {
|
||||
addr, err := addrForDial(req.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bootstrapConn, err := utlsDialContextWithOptions(req.Context(), "tcp", addr, config, id, forceIPv4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Peek at the ALPN-negotiated protocol.
|
||||
protocol := bootstrapConn.ConnectionState().NegotiatedProtocol
|
||||
|
||||
// Protects bootstrapConn.
|
||||
var lock sync.Mutex
|
||||
// This is the callback for future dials done by the inner
|
||||
// http.Transport or http2.Transport.
|
||||
dialTLSContext := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
// On the first dial, reuse bootstrapConn.
|
||||
if bootstrapConn != nil {
|
||||
uconn := bootstrapConn
|
||||
bootstrapConn = nil
|
||||
return uconn, nil
|
||||
}
|
||||
|
||||
// Later dials make a new connection.
|
||||
uconn, err := utlsDialContextWithOptions(ctx, "tcp", addr, config, id, forceIPv4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if uconn.ConnectionState().NegotiatedProtocol != protocol {
|
||||
return nil, fmt.Errorf("unexpected switch from ALPN %q to %q",
|
||||
protocol, uconn.ConnectionState().NegotiatedProtocol)
|
||||
}
|
||||
|
||||
return uconn, nil
|
||||
}
|
||||
|
||||
// Construct an http.Transport or http2.Transport depending on ALPN.
|
||||
switch protocol {
|
||||
case http2.NextProtoTLS:
|
||||
// Unfortunately http2.Transport does not expose the same
|
||||
// configuration options as http.Transport with regard to
|
||||
// timeouts, etc., so we are at the mercy of the defaults.
|
||||
// https://github.com/golang/go/issues/16581
|
||||
return &http2.Transport{
|
||||
DialTLS: func(network, addr string, _ *tls.Config) (net.Conn, error) {
|
||||
// Ignore the *tls.Config parameter; use our
|
||||
// static config instead.
|
||||
return dialTLSContext(context.Background(), network, addr)
|
||||
},
|
||||
}, nil
|
||||
default:
|
||||
// With http.Transport, copy important default fields from
|
||||
// http.DefaultTransport, such as TLSHandshakeTimeout and
|
||||
// IdleConnTimeout, before overriding DialTLSContext.
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.DialTLSContext = dialTLSContext
|
||||
return tr, nil
|
||||
}
|
||||
}
|
||||
|
||||
// addrForDial extracts a host:port address from a URL, suitable for dialing.
|
||||
func addrForDial(url *url.URL) (string, error) {
|
||||
host := url.Hostname()
|
||||
// net/http would use golang.org/x/net/idna here, to convert a possible
|
||||
// internationalized domain name to ASCII.
|
||||
port := url.Port()
|
||||
if port == "" {
|
||||
// No port? Use the default for the scheme.
|
||||
switch url.Scheme {
|
||||
case "http":
|
||||
port = "80"
|
||||
case "https":
|
||||
port = "443"
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported URL scheme %q", url.Scheme)
|
||||
}
|
||||
}
|
||||
return net.JoinHostPort(host, port), nil
|
||||
}
|
||||
201
internal/dnsttclient/weightedlist.go
Normal file
201
internal/dnsttclient/weightedlist.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package dnsttclient
|
||||
|
||||
// Random selection from weighted distributions, and strings for specifying such
|
||||
// distributions.
|
||||
|
||||
import (
|
||||
cryptorand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
mathrand "math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// parseWeightedList parses a list of text labels with optional numeric weights,
|
||||
// and returns parallel slices of weights and labels. If a weight is omitted for
|
||||
// a label, the weight is 1.
|
||||
//
|
||||
// An example weighted list string is "2*apple,orange,10*cookie". This example
|
||||
// results in the slices [2, 1, 10] and ["apple", "orange", "cookie"].
|
||||
// Bytes may be escaped by backslashes.
|
||||
//
|
||||
// list ::= entry ("," entry)*
|
||||
// entry ::= (weight "*")? label
|
||||
func parseWeightedList(s string) ([]uint32, []string, error) {
|
||||
const (
|
||||
kindEOF = iota
|
||||
kindComma
|
||||
kindAsterisk
|
||||
kindText
|
||||
kindError
|
||||
)
|
||||
type token struct {
|
||||
Kind int
|
||||
Text string
|
||||
}
|
||||
|
||||
var i int
|
||||
// nextToken incrementally consumes s and returns tokens.
|
||||
nextToken := func() token {
|
||||
if !(i < len(s)) {
|
||||
return token{Kind: kindEOF}
|
||||
}
|
||||
if s[i] == ',' {
|
||||
i++
|
||||
return token{Kind: kindComma}
|
||||
}
|
||||
if s[i] == '*' {
|
||||
i++
|
||||
return token{Kind: kindAsterisk}
|
||||
}
|
||||
var text strings.Builder
|
||||
for i < len(s) && s[i] != ',' && s[i] != '*' {
|
||||
if s[i] == '\\' {
|
||||
i++
|
||||
if !(i < len(s)) {
|
||||
return token{Kind: kindError, Text: fmt.Sprintf("%q at end of string", s[i])}
|
||||
}
|
||||
}
|
||||
text.WriteByte(s[i])
|
||||
i++
|
||||
}
|
||||
return token{Kind: kindText, Text: text.String()}
|
||||
}
|
||||
peekToken := func() token {
|
||||
saved := i
|
||||
t := nextToken()
|
||||
i = saved
|
||||
return t
|
||||
}
|
||||
|
||||
const (
|
||||
stateBeginEntry = iota
|
||||
stateLabel
|
||||
stateEndEntry
|
||||
stateDone
|
||||
stateUnexpected
|
||||
)
|
||||
|
||||
var weights []uint32
|
||||
var labels []string
|
||||
var weightString, label string
|
||||
var t token
|
||||
for state := stateBeginEntry; state != stateDone; {
|
||||
switch state {
|
||||
// Beginning of a new entry (at the beginning of the input or
|
||||
// after a comma).
|
||||
case stateBeginEntry:
|
||||
t = nextToken()
|
||||
switch t.Kind {
|
||||
case kindText:
|
||||
// If the next token is an asterisk, this text
|
||||
// represents a weight; otherwise it represents
|
||||
// a label (with a weight of "1").
|
||||
switch peekToken().Kind {
|
||||
case kindAsterisk:
|
||||
nextToken() // Consume the asterisk token.
|
||||
weightString = t.Text
|
||||
state = stateLabel
|
||||
default:
|
||||
weightString = "1"
|
||||
label = t.Text
|
||||
state = stateEndEntry
|
||||
}
|
||||
default:
|
||||
state = stateUnexpected
|
||||
}
|
||||
// weightString is assigned and we have seen an asterisk, now
|
||||
// expect a text label.
|
||||
case stateLabel:
|
||||
t = nextToken()
|
||||
switch t.Kind {
|
||||
case kindText:
|
||||
label = t.Text
|
||||
state = stateEndEntry
|
||||
default:
|
||||
state = stateUnexpected
|
||||
}
|
||||
// weightString and label are assigned, now emit the entry and
|
||||
// expect a comma or EOF.
|
||||
case stateEndEntry:
|
||||
w, err := strconv.ParseUint(weightString, 10, 32)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
weights = append(weights, uint32(w))
|
||||
labels = append(labels, label)
|
||||
t = nextToken()
|
||||
switch t.Kind {
|
||||
case kindEOF:
|
||||
state = stateDone
|
||||
case kindComma:
|
||||
state = stateBeginEntry
|
||||
default:
|
||||
state = stateUnexpected
|
||||
}
|
||||
case stateUnexpected:
|
||||
if t.Kind == kindError {
|
||||
return nil, nil, fmt.Errorf("%s", t.Text)
|
||||
} else {
|
||||
var ttext string
|
||||
switch t.Kind {
|
||||
case kindEOF:
|
||||
ttext = "end of string"
|
||||
case kindComma:
|
||||
ttext = "\",\""
|
||||
case kindAsterisk:
|
||||
ttext = "\"*\""
|
||||
case kindText:
|
||||
ttext = fmt.Sprintf("%+q", t.Text)
|
||||
}
|
||||
return nil, nil, fmt.Errorf("unexpected %s", ttext)
|
||||
}
|
||||
default:
|
||||
panic(state)
|
||||
}
|
||||
}
|
||||
|
||||
return weights, labels, nil
|
||||
}
|
||||
|
||||
// cryptoSource is a math/rand Source that reads from the crypto/rand Reader.
|
||||
// The Seed method does not affect the sequence of numbers returned from the
|
||||
// Int63 method.
|
||||
type cryptoSource struct{}
|
||||
|
||||
func (s cryptoSource) Seed(_ int64) {}
|
||||
|
||||
func (s cryptoSource) Int63() int64 {
|
||||
var n int64
|
||||
err := binary.Read(cryptorand.Reader, binary.BigEndian, &n)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
n &= (1 << 63) - 1
|
||||
return n
|
||||
}
|
||||
|
||||
// sampleWeighted returns the index of a randomly selected element of the
|
||||
// weights slice, weighted by the values stored in the slice. Panics if
|
||||
// the sum of the weights is zero or does not fit in an int64.
|
||||
func sampleWeighted(weights []uint32) int {
|
||||
var sum int64 = 0
|
||||
for _, w := range weights {
|
||||
sum += int64(w)
|
||||
if sum < int64(w) {
|
||||
panic("weights overflow")
|
||||
}
|
||||
}
|
||||
if sum == 0 {
|
||||
panic("total weight is zero")
|
||||
}
|
||||
r := uint64(mathrand.New(&cryptoSource{}).Int63n(sum))
|
||||
for i, w := range weights {
|
||||
if r < uint64(w) {
|
||||
return i
|
||||
}
|
||||
r -= uint64(w)
|
||||
}
|
||||
panic("impossible")
|
||||
}
|
||||
575
internal/dnsttcore/dns/dns.go
Normal file
575
internal/dnsttcore/dns/dns.go
Normal file
@@ -0,0 +1,575 @@
|
||||
// Package dns deals with encoding and decoding DNS wire format.
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The maximum number of DNS name compression pointers we are willing to follow.
|
||||
// Without something like this, infinite loops are possible.
|
||||
const compressionPointerLimit = 10
|
||||
|
||||
var (
|
||||
// ErrZeroLengthLabel is the error returned for names that contain a
|
||||
// zero-length label, like "example..com".
|
||||
ErrZeroLengthLabel = errors.New("name contains a zero-length label")
|
||||
|
||||
// ErrLabelTooLong is the error returned for labels that are longer than
|
||||
// 63 octets.
|
||||
ErrLabelTooLong = errors.New("name contains a label longer than 63 octets")
|
||||
|
||||
// ErrNameTooLong is the error returned for names whose encoded
|
||||
// representation is longer than 255 octets.
|
||||
ErrNameTooLong = errors.New("name is longer than 255 octets")
|
||||
|
||||
// ErrReservedLabelType is the error returned when reading a label type
|
||||
// prefix whose two most significant bits are not 00 or 11.
|
||||
ErrReservedLabelType = errors.New("reserved label type")
|
||||
|
||||
// ErrTooManyPointers is the error returned when reading a compressed
|
||||
// name that has too many compression pointers.
|
||||
ErrTooManyPointers = errors.New("too many compression pointers")
|
||||
|
||||
// ErrTrailingBytes is the error returned when bytes remain in the parse
|
||||
// buffer after parsing a message.
|
||||
ErrTrailingBytes = errors.New("trailing bytes after message")
|
||||
|
||||
// ErrIntegerOverflow is the error returned when trying to encode an
|
||||
// integer greater than 65535 into a 16-bit field.
|
||||
ErrIntegerOverflow = errors.New("integer overflow")
|
||||
)
|
||||
|
||||
const (
|
||||
// https://tools.ietf.org/html/rfc1035#section-3.2.2
|
||||
RRTypeTXT = 16
|
||||
// https://tools.ietf.org/html/rfc6891#section-6.1.1
|
||||
RRTypeOPT = 41
|
||||
|
||||
// https://tools.ietf.org/html/rfc1035#section-3.2.4
|
||||
ClassIN = 1
|
||||
|
||||
// https://tools.ietf.org/html/rfc1035#section-4.1.1
|
||||
RcodeNoError = 0 // a.k.a. NOERROR
|
||||
RcodeFormatError = 1 // a.k.a. FORMERR
|
||||
RcodeNameError = 3 // a.k.a. NXDOMAIN
|
||||
RcodeNotImplemented = 4 // a.k.a. NOTIMPL
|
||||
// https://tools.ietf.org/html/rfc6891#section-9
|
||||
ExtendedRcodeBadVers = 16 // a.k.a. BADVERS
|
||||
)
|
||||
|
||||
// Name represents a domain name, a sequence of labels each of which is 63
|
||||
// octets or less in length.
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc1035#section-3.1
|
||||
type Name [][]byte
|
||||
|
||||
// NewName returns a Name from a slice of labels, after checking the labels for
|
||||
// validity. Does not include a zero-length label at the end of the slice.
|
||||
func NewName(labels [][]byte) (Name, error) {
|
||||
name := Name(labels)
|
||||
// https://tools.ietf.org/html/rfc1035#section-2.3.4
|
||||
// Various objects and parameters in the DNS have size limits.
|
||||
// labels 63 octets or less
|
||||
// names 255 octets or less
|
||||
for _, label := range labels {
|
||||
if len(label) == 0 {
|
||||
return nil, ErrZeroLengthLabel
|
||||
}
|
||||
if len(label) > 63 {
|
||||
return nil, ErrLabelTooLong
|
||||
}
|
||||
}
|
||||
// Check the total length.
|
||||
builder := newMessageBuilder()
|
||||
builder.WriteName(name)
|
||||
if len(builder.Bytes()) > 255 {
|
||||
return nil, ErrNameTooLong
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// ParseName returns a new Name from a string of labels separated by dots, after
|
||||
// checking the name for validity. A single dot at the end of the string is
|
||||
// ignored.
|
||||
func ParseName(s string) (Name, error) {
|
||||
b := bytes.TrimSuffix([]byte(s), []byte("."))
|
||||
if len(b) == 0 {
|
||||
// bytes.Split(b, ".") would return [""] in this case
|
||||
return NewName([][]byte{})
|
||||
} else {
|
||||
return NewName(bytes.Split(b, []byte(".")))
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a reversible string representation of name. Labels are
|
||||
// separated by dots, and any bytes in a label that are outside the set
|
||||
// [0-9A-Za-z-] are replaced with a \xXX hex escape sequence.
|
||||
func (name Name) String() string {
|
||||
if len(name) == 0 {
|
||||
return "."
|
||||
}
|
||||
|
||||
var buf strings.Builder
|
||||
for i, label := range name {
|
||||
if i > 0 {
|
||||
buf.WriteByte('.')
|
||||
}
|
||||
for _, b := range label {
|
||||
if b == '-' ||
|
||||
('0' <= b && b <= '9') ||
|
||||
('A' <= b && b <= 'Z') ||
|
||||
('a' <= b && b <= 'z') {
|
||||
buf.WriteByte(b)
|
||||
} else {
|
||||
fmt.Fprintf(&buf, "\\x%02x", b)
|
||||
}
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// TrimSuffix returns a Name with the given suffix removed, if it was present.
|
||||
// The second return value indicates whether the suffix was present. If the
|
||||
// suffix was not present, the first return value is nil.
|
||||
func (name Name) TrimSuffix(suffix Name) (Name, bool) {
|
||||
if len(name) < len(suffix) {
|
||||
return nil, false
|
||||
}
|
||||
split := len(name) - len(suffix)
|
||||
fore, aft := name[:split], name[split:]
|
||||
for i := 0; i < len(aft); i++ {
|
||||
if !bytes.Equal(bytes.ToLower(aft[i]), bytes.ToLower(suffix[i])) {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return fore, true
|
||||
}
|
||||
|
||||
// Message represents a DNS message.
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc1035#section-4.1
|
||||
type Message struct {
|
||||
ID uint16
|
||||
Flags uint16
|
||||
|
||||
Question []Question
|
||||
Answer []RR
|
||||
Authority []RR
|
||||
Additional []RR
|
||||
}
|
||||
|
||||
// Opcode extracts the OPCODE part of the Flags field.
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc1035#section-4.1.1
|
||||
func (message *Message) Opcode() uint16 {
|
||||
return (message.Flags >> 11) & 0xf
|
||||
}
|
||||
|
||||
// Rcode extracts the RCODE part of the Flags field.
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc1035#section-4.1.1
|
||||
func (message *Message) Rcode() uint16 {
|
||||
return message.Flags & 0x000f
|
||||
}
|
||||
|
||||
// Question represents an entry in the question section of a message.
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc1035#section-4.1.2
|
||||
type Question struct {
|
||||
Name Name
|
||||
Type uint16
|
||||
Class uint16
|
||||
}
|
||||
|
||||
// RR represents a resource record.
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc1035#section-4.1.3
|
||||
type RR struct {
|
||||
Name Name
|
||||
Type uint16
|
||||
Class uint16
|
||||
TTL uint32
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// readName parses a DNS name from r. It leaves r positioned just after the
|
||||
// parsed name.
|
||||
func readName(r io.ReadSeeker) (Name, error) {
|
||||
var labels [][]byte
|
||||
// We limit the number of compression pointers we are willing to follow.
|
||||
numPointers := 0
|
||||
// If we followed any compression pointers, we must finally seek to just
|
||||
// past the first pointer.
|
||||
var seekTo int64
|
||||
loop:
|
||||
for {
|
||||
var labelType byte
|
||||
err := binary.Read(r, binary.BigEndian, &labelType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch labelType & 0xc0 {
|
||||
case 0x00:
|
||||
// This is an ordinary label.
|
||||
// https://tools.ietf.org/html/rfc1035#section-3.1
|
||||
length := int(labelType & 0x3f)
|
||||
if length == 0 {
|
||||
break loop
|
||||
}
|
||||
label := make([]byte, length)
|
||||
_, err := io.ReadFull(r, label)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
labels = append(labels, label)
|
||||
case 0xc0:
|
||||
// This is a compression pointer.
|
||||
// https://tools.ietf.org/html/rfc1035#section-4.1.4
|
||||
upper := labelType & 0x3f
|
||||
var lower byte
|
||||
err := binary.Read(r, binary.BigEndian, &lower)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
offset := (uint16(upper) << 8) | uint16(lower)
|
||||
|
||||
if numPointers == 0 {
|
||||
// The first time we encounter a pointer,
|
||||
// remember our position so we can seek back to
|
||||
// it when done.
|
||||
seekTo, err = r.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
numPointers++
|
||||
if numPointers > compressionPointerLimit {
|
||||
return nil, ErrTooManyPointers
|
||||
}
|
||||
|
||||
// Follow the pointer and continue.
|
||||
_, err = r.Seek(int64(offset), io.SeekStart)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
// "The 10 and 01 combinations are reserved for future
|
||||
// use."
|
||||
return nil, ErrReservedLabelType
|
||||
}
|
||||
}
|
||||
// If we followed any pointers, then seek back to just after the first
|
||||
// one.
|
||||
if numPointers > 0 {
|
||||
_, err := r.Seek(seekTo, io.SeekStart)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return NewName(labels)
|
||||
}
|
||||
|
||||
// readQuestion parses one entry from the Question section. It leaves r
|
||||
// positioned just after the parsed entry.
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc1035#section-4.1.2
|
||||
func readQuestion(r io.ReadSeeker) (Question, error) {
|
||||
var question Question
|
||||
var err error
|
||||
question.Name, err = readName(r)
|
||||
if err != nil {
|
||||
return question, err
|
||||
}
|
||||
for _, ptr := range []*uint16{&question.Type, &question.Class} {
|
||||
err := binary.Read(r, binary.BigEndian, ptr)
|
||||
if err != nil {
|
||||
return question, err
|
||||
}
|
||||
}
|
||||
|
||||
return question, nil
|
||||
}
|
||||
|
||||
// readRR parses one resource record. It leaves r positioned just after the
|
||||
// parsed resource record.
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc1035#section-4.1.3
|
||||
func readRR(r io.ReadSeeker) (RR, error) {
|
||||
var rr RR
|
||||
var err error
|
||||
rr.Name, err = readName(r)
|
||||
if err != nil {
|
||||
return rr, err
|
||||
}
|
||||
for _, ptr := range []*uint16{&rr.Type, &rr.Class} {
|
||||
err := binary.Read(r, binary.BigEndian, ptr)
|
||||
if err != nil {
|
||||
return rr, err
|
||||
}
|
||||
}
|
||||
err = binary.Read(r, binary.BigEndian, &rr.TTL)
|
||||
if err != nil {
|
||||
return rr, err
|
||||
}
|
||||
var rdLength uint16
|
||||
err = binary.Read(r, binary.BigEndian, &rdLength)
|
||||
if err != nil {
|
||||
return rr, err
|
||||
}
|
||||
rr.Data = make([]byte, rdLength)
|
||||
_, err = io.ReadFull(r, rr.Data)
|
||||
if err != nil {
|
||||
return rr, err
|
||||
}
|
||||
|
||||
return rr, nil
|
||||
}
|
||||
|
||||
// readMessage parses a complete DNS message. It leaves r positioned just after
|
||||
// the parsed message.
|
||||
func readMessage(r io.ReadSeeker) (Message, error) {
|
||||
var message Message
|
||||
|
||||
// Header section
|
||||
// https://tools.ietf.org/html/rfc1035#section-4.1.1
|
||||
var qdCount, anCount, nsCount, arCount uint16
|
||||
for _, ptr := range []*uint16{
|
||||
&message.ID, &message.Flags,
|
||||
&qdCount, &anCount, &nsCount, &arCount,
|
||||
} {
|
||||
err := binary.Read(r, binary.BigEndian, ptr)
|
||||
if err != nil {
|
||||
return message, err
|
||||
}
|
||||
}
|
||||
|
||||
// Question section
|
||||
// https://tools.ietf.org/html/rfc1035#section-4.1.2
|
||||
for i := 0; i < int(qdCount); i++ {
|
||||
question, err := readQuestion(r)
|
||||
if err != nil {
|
||||
return message, err
|
||||
}
|
||||
message.Question = append(message.Question, question)
|
||||
}
|
||||
|
||||
// Answer, Authority, and Additional sections
|
||||
// https://tools.ietf.org/html/rfc1035#section-4.1.3
|
||||
for _, rec := range []struct {
|
||||
ptr *[]RR
|
||||
count uint16
|
||||
}{
|
||||
{&message.Answer, anCount},
|
||||
{&message.Authority, nsCount},
|
||||
{&message.Additional, arCount},
|
||||
} {
|
||||
for i := 0; i < int(rec.count); i++ {
|
||||
rr, err := readRR(r)
|
||||
if err != nil {
|
||||
return message, err
|
||||
}
|
||||
*rec.ptr = append(*rec.ptr, rr)
|
||||
}
|
||||
}
|
||||
|
||||
return message, nil
|
||||
}
|
||||
|
||||
// MessageFromWireFormat parses a message from buf and returns a Message object.
|
||||
// It returns ErrTrailingBytes if there are bytes remaining in buf after parsing
|
||||
// is done.
|
||||
func MessageFromWireFormat(buf []byte) (Message, error) {
|
||||
r := bytes.NewReader(buf)
|
||||
message, err := readMessage(r)
|
||||
if err == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
} else if err == nil {
|
||||
// Check for trailing bytes.
|
||||
_, err = r.ReadByte()
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
} else if err == nil {
|
||||
err = ErrTrailingBytes
|
||||
}
|
||||
}
|
||||
return message, err
|
||||
}
|
||||
|
||||
// messageBuilder manages the state of serializing a DNS message. Its main
|
||||
// function is to keep track of names already written for the purpose of name
|
||||
// compression.
|
||||
type messageBuilder struct {
|
||||
w bytes.Buffer
|
||||
nameCache map[string]int
|
||||
}
|
||||
|
||||
// newMessageBuilder creates a new messageBuilder with an empty name cache.
|
||||
func newMessageBuilder() *messageBuilder {
|
||||
return &messageBuilder{
|
||||
nameCache: make(map[string]int),
|
||||
}
|
||||
}
|
||||
|
||||
// Bytes returns the serialized DNS message as a slice of bytes.
|
||||
func (builder *messageBuilder) Bytes() []byte {
|
||||
return builder.w.Bytes()
|
||||
}
|
||||
|
||||
// WriteName appends name to the in-progress messageBuilder, employing
|
||||
// compression pointers to previously written names if possible.
|
||||
func (builder *messageBuilder) WriteName(name Name) {
|
||||
// https://tools.ietf.org/html/rfc1035#section-3.1
|
||||
for i := range name {
|
||||
// Has this suffix already been encoded in the message?
|
||||
if ptr, ok := builder.nameCache[name[i:].String()]; ok && ptr&0x3fff == ptr {
|
||||
// If so, we can write a compression pointer.
|
||||
binary.Write(&builder.w, binary.BigEndian, uint16(0xc000|ptr))
|
||||
return
|
||||
}
|
||||
// Not cached; we must encode this label verbatim. Store a cache
|
||||
// entry pointing to the beginning of it.
|
||||
builder.nameCache[name[i:].String()] = builder.w.Len()
|
||||
length := len(name[i])
|
||||
if length == 0 || length > 63 {
|
||||
panic(length)
|
||||
}
|
||||
builder.w.WriteByte(byte(length))
|
||||
builder.w.Write(name[i])
|
||||
}
|
||||
builder.w.WriteByte(0)
|
||||
}
|
||||
|
||||
// WriteQuestion appends a Question section entry to the in-progress
|
||||
// messageBuilder.
|
||||
func (builder *messageBuilder) WriteQuestion(question *Question) {
|
||||
// https://tools.ietf.org/html/rfc1035#section-4.1.2
|
||||
builder.WriteName(question.Name)
|
||||
binary.Write(&builder.w, binary.BigEndian, question.Type)
|
||||
binary.Write(&builder.w, binary.BigEndian, question.Class)
|
||||
}
|
||||
|
||||
// WriteRR appends a resource record to the in-progress messageBuilder. It
|
||||
// returns ErrIntegerOverflow if the length of rr.Data does not fit in 16 bits.
|
||||
func (builder *messageBuilder) WriteRR(rr *RR) error {
|
||||
// https://tools.ietf.org/html/rfc1035#section-4.1.3
|
||||
builder.WriteName(rr.Name)
|
||||
binary.Write(&builder.w, binary.BigEndian, rr.Type)
|
||||
binary.Write(&builder.w, binary.BigEndian, rr.Class)
|
||||
binary.Write(&builder.w, binary.BigEndian, rr.TTL)
|
||||
rdLength := uint16(len(rr.Data))
|
||||
if int(rdLength) != len(rr.Data) {
|
||||
return ErrIntegerOverflow
|
||||
}
|
||||
binary.Write(&builder.w, binary.BigEndian, rdLength)
|
||||
builder.w.Write(rr.Data)
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteMessage appends a complete DNS message to the in-progress
|
||||
// messageBuilder. It returns ErrIntegerOverflow if the number of entries in any
|
||||
// section, or the length of the data in any resource record, does not fit in 16
|
||||
// bits.
|
||||
func (builder *messageBuilder) WriteMessage(message *Message) error {
|
||||
// Header section
|
||||
// https://tools.ietf.org/html/rfc1035#section-4.1.1
|
||||
binary.Write(&builder.w, binary.BigEndian, message.ID)
|
||||
binary.Write(&builder.w, binary.BigEndian, message.Flags)
|
||||
for _, count := range []int{
|
||||
len(message.Question),
|
||||
len(message.Answer),
|
||||
len(message.Authority),
|
||||
len(message.Additional),
|
||||
} {
|
||||
count16 := uint16(count)
|
||||
if int(count16) != count {
|
||||
return ErrIntegerOverflow
|
||||
}
|
||||
binary.Write(&builder.w, binary.BigEndian, count16)
|
||||
}
|
||||
|
||||
// Question section
|
||||
// https://tools.ietf.org/html/rfc1035#section-4.1.2
|
||||
for _, question := range message.Question {
|
||||
builder.WriteQuestion(&question)
|
||||
}
|
||||
|
||||
// Answer, Authority, and Additional sections
|
||||
// https://tools.ietf.org/html/rfc1035#section-4.1.3
|
||||
for _, rrs := range [][]RR{message.Answer, message.Authority, message.Additional} {
|
||||
for _, rr := range rrs {
|
||||
err := builder.WriteRR(&rr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WireFormat encodes a Message as a slice of bytes in DNS wire format. It
|
||||
// returns ErrIntegerOverflow if the number of entries in any section, or the
|
||||
// length of the data in any resource record, does not fit in 16 bits.
|
||||
func (message *Message) WireFormat() ([]byte, error) {
|
||||
builder := newMessageBuilder()
|
||||
err := builder.WriteMessage(message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return builder.Bytes(), nil
|
||||
}
|
||||
|
||||
// DecodeRDataTXT decodes TXT-DATA (as found in the RDATA for a resource record
|
||||
// with TYPE=TXT) as a raw byte slice, by concatenating all the
|
||||
// <character-string>s it contains.
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc1035#section-3.3.14
|
||||
func DecodeRDataTXT(p []byte) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
for {
|
||||
if len(p) == 0 {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
n := int(p[0])
|
||||
p = p[1:]
|
||||
if len(p) < n {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
buf.Write(p[:n])
|
||||
p = p[n:]
|
||||
if len(p) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// EncodeRDataTXT encodes a slice of bytes as TXT-DATA, as appropriate for the
|
||||
// RDATA of a resource record with TYPE=TXT. No length restriction is enforced
|
||||
// here; that must be checked at a higher level.
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc1035#section-3.3.14
|
||||
func EncodeRDataTXT(p []byte) []byte {
|
||||
// https://tools.ietf.org/html/rfc1035#section-3.3
|
||||
// https://tools.ietf.org/html/rfc1035#section-3.3.14
|
||||
// TXT data is a sequence of one or more <character-string>s, where
|
||||
// <character-string> is a length octet followed by that number of
|
||||
// octets.
|
||||
var buf bytes.Buffer
|
||||
for len(p) > 255 {
|
||||
buf.WriteByte(255)
|
||||
buf.Write(p[:255])
|
||||
p = p[255:]
|
||||
}
|
||||
// Must write here, even if len(p) == 0, because it's "*one or more*
|
||||
// <character-string>s".
|
||||
buf.WriteByte(byte(len(p)))
|
||||
buf.Write(p)
|
||||
return buf.Bytes()
|
||||
}
|
||||
592
internal/dnsttcore/dns/dns_test.go
Normal file
592
internal/dnsttcore/dns/dns_test.go
Normal file
@@ -0,0 +1,592 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func namesEqual(a, b Name) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(a); i++ {
|
||||
if !bytes.Equal(a[i], b[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
labels [][]byte
|
||||
err error
|
||||
s string
|
||||
}{
|
||||
{[][]byte{}, nil, "."},
|
||||
{[][]byte{[]byte("test")}, nil, "test"},
|
||||
{[][]byte{[]byte("a"), []byte("b"), []byte("c")}, nil, "a.b.c"},
|
||||
|
||||
{[][]byte{{}}, ErrZeroLengthLabel, ""},
|
||||
{[][]byte{[]byte("a"), {}, []byte("c")}, ErrZeroLengthLabel, ""},
|
||||
|
||||
// 63 octets.
|
||||
{[][]byte{[]byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE")}, nil,
|
||||
"0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE"},
|
||||
// 64 octets.
|
||||
{[][]byte{[]byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDEF")}, ErrLabelTooLong, ""},
|
||||
|
||||
// 64+64+64+62 octets.
|
||||
{[][]byte{
|
||||
[]byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE"),
|
||||
[]byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE"),
|
||||
[]byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE"),
|
||||
[]byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABC"),
|
||||
}, nil,
|
||||
"0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE.0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE.0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE.0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABC"},
|
||||
// 64+64+64+63 octets.
|
||||
{[][]byte{
|
||||
[]byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE"),
|
||||
[]byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE"),
|
||||
[]byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDE"),
|
||||
[]byte("0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCD"),
|
||||
}, ErrNameTooLong, ""},
|
||||
// 127 one-octet labels.
|
||||
{[][]byte{
|
||||
{'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'},
|
||||
{'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'}, {'F'},
|
||||
{'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'},
|
||||
{'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'}, {'F'},
|
||||
{'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'},
|
||||
{'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'}, {'F'},
|
||||
{'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'},
|
||||
{'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'},
|
||||
}, nil,
|
||||
"0.1.2.3.4.5.6.7.8.9.a.b.c.d.e.f.0.1.2.3.4.5.6.7.8.9.A.B.C.D.E.F.0.1.2.3.4.5.6.7.8.9.a.b.c.d.e.f.0.1.2.3.4.5.6.7.8.9.A.B.C.D.E.F.0.1.2.3.4.5.6.7.8.9.a.b.c.d.e.f.0.1.2.3.4.5.6.7.8.9.A.B.C.D.E.F.0.1.2.3.4.5.6.7.8.9.a.b.c.d.e.f.0.1.2.3.4.5.6.7.8.9.A.B.C.D.E"},
|
||||
// 128 one-octet labels.
|
||||
{[][]byte{
|
||||
{'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'},
|
||||
{'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'}, {'F'},
|
||||
{'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'},
|
||||
{'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'}, {'F'},
|
||||
{'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'},
|
||||
{'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'}, {'F'},
|
||||
{'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'},
|
||||
{'0'}, {'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}, {'7'}, {'8'}, {'9'}, {'A'}, {'B'}, {'C'}, {'D'}, {'E'}, {'F'},
|
||||
}, ErrNameTooLong, ""},
|
||||
} {
|
||||
// Test that NewName returns proper error codes, and otherwise
|
||||
// returns an equal slice of labels.
|
||||
name, err := NewName(test.labels)
|
||||
if err != test.err || (err == nil && !namesEqual(name, test.labels)) {
|
||||
t.Errorf("%+q returned (%+q, %v), expected (%+q, %v)",
|
||||
test.labels, name, err, test.labels, test.err)
|
||||
continue
|
||||
}
|
||||
if test.err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Test that the string version of the name comes out as
|
||||
// expected.
|
||||
s := name.String()
|
||||
if s != test.s {
|
||||
t.Errorf("%+q became string %+q, expected %+q", test.labels, s, test.s)
|
||||
continue
|
||||
}
|
||||
|
||||
// Test that parsing from a string back to a Name results in the
|
||||
// original slice of labels.
|
||||
name, err = ParseName(s)
|
||||
if err != nil || !namesEqual(name, test.labels) {
|
||||
t.Errorf("%+q parsing %+q returned (%+q, %v), expected (%+q, %v)",
|
||||
test.labels, s, name, err, test.labels, nil)
|
||||
continue
|
||||
}
|
||||
// A trailing dot should be ignored.
|
||||
if !strings.HasSuffix(s, ".") {
|
||||
dotName, dotErr := ParseName(s + ".")
|
||||
if dotErr != err || !namesEqual(dotName, name) {
|
||||
t.Errorf("%+q parsing %+q returned (%+q, %v), expected (%+q, %v)",
|
||||
test.labels, s+".", dotName, dotErr, name, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseName(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
s string
|
||||
name Name
|
||||
err error
|
||||
}{
|
||||
// This case can't be tested by TestName above because String
|
||||
// will never produce "" (it produces "." instead).
|
||||
{"", [][]byte{}, nil},
|
||||
} {
|
||||
name, err := ParseName(test.s)
|
||||
if err != test.err || (err == nil && !namesEqual(name, test.name)) {
|
||||
t.Errorf("%+q returned (%+q, %v), expected (%+q, %v)",
|
||||
test.s, name, err, test.name, test.err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func unescapeString(s string) ([][]byte, error) {
|
||||
if s == "." {
|
||||
return [][]byte{}, nil
|
||||
}
|
||||
|
||||
var result [][]byte
|
||||
for _, label := range strings.Split(s, ".") {
|
||||
var buf bytes.Buffer
|
||||
i := 0
|
||||
for i < len(label) {
|
||||
switch label[i] {
|
||||
case '\\':
|
||||
if i+3 >= len(label) {
|
||||
return nil, fmt.Errorf("truncated escape sequence at index %v", i)
|
||||
}
|
||||
if label[i+1] != 'x' {
|
||||
return nil, fmt.Errorf("malformed escape sequence at index %v", i)
|
||||
}
|
||||
b, err := strconv.ParseUint(string(label[i+2:i+4]), 16, 8)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed hex sequence at index %v", i+2)
|
||||
}
|
||||
buf.WriteByte(byte(b))
|
||||
i += 4
|
||||
default:
|
||||
buf.WriteByte(label[i])
|
||||
i++
|
||||
}
|
||||
}
|
||||
result = append(result, buf.Bytes())
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func TestNameString(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name Name
|
||||
s string
|
||||
}{
|
||||
{[][]byte{}, "."},
|
||||
{[][]byte{[]byte("\x00"), []byte("a.b"), []byte("c\nd\\")}, "\\x00.a\\x2eb.c\\x0ad\\x5c"},
|
||||
{[][]byte{
|
||||
[]byte("\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>"),
|
||||
[]byte("?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}"),
|
||||
[]byte("~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc"),
|
||||
[]byte("\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb"),
|
||||
[]byte("\xfc\xfd\xfe\xff"),
|
||||
}, "\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x09\\x0a\\x0b\\x0c\\x0d\\x0e\\x0f\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1a\\x1b\\x1c\\x1d\\x1e\\x1f\\x20\\x21\\x22\\x23\\x24\\x25\\x26\\x27\\x28\\x29\\x2a\\x2b\\x2c-\\x2e\\x2f0123456789\\x3a\\x3b\\x3c\\x3d\\x3e.\\x3f\\x40ABCDEFGHIJKLMNOPQRSTUVWXYZ\\x5b\\x5c\\x5d\\x5e\\x5f\\x60abcdefghijklmnopqrstuvwxyz\\x7b\\x7c\\x7d.\\x7e\\x7f\\x80\\x81\\x82\\x83\\x84\\x85\\x86\\x87\\x88\\x89\\x8a\\x8b\\x8c\\x8d\\x8e\\x8f\\x90\\x91\\x92\\x93\\x94\\x95\\x96\\x97\\x98\\x99\\x9a\\x9b\\x9c\\x9d\\x9e\\x9f\\xa0\\xa1\\xa2\\xa3\\xa4\\xa5\\xa6\\xa7\\xa8\\xa9\\xaa\\xab\\xac\\xad\\xae\\xaf\\xb0\\xb1\\xb2\\xb3\\xb4\\xb5\\xb6\\xb7\\xb8\\xb9\\xba\\xbb\\xbc.\\xbd\\xbe\\xbf\\xc0\\xc1\\xc2\\xc3\\xc4\\xc5\\xc6\\xc7\\xc8\\xc9\\xca\\xcb\\xcc\\xcd\\xce\\xcf\\xd0\\xd1\\xd2\\xd3\\xd4\\xd5\\xd6\\xd7\\xd8\\xd9\\xda\\xdb\\xdc\\xdd\\xde\\xdf\\xe0\\xe1\\xe2\\xe3\\xe4\\xe5\\xe6\\xe7\\xe8\\xe9\\xea\\xeb\\xec\\xed\\xee\\xef\\xf0\\xf1\\xf2\\xf3\\xf4\\xf5\\xf6\\xf7\\xf8\\xf9\\xfa\\xfb.\\xfc\\xfd\\xfe\\xff"},
|
||||
} {
|
||||
s := test.name.String()
|
||||
if s != test.s {
|
||||
t.Errorf("%+q escaped to %+q, expected %+q", test.name, s, test.s)
|
||||
continue
|
||||
}
|
||||
unescaped, err := unescapeString(s)
|
||||
if err != nil {
|
||||
t.Errorf("%+q unescaping %+q resulted in error %v", test.name, s, err)
|
||||
continue
|
||||
}
|
||||
if !namesEqual(Name(unescaped), test.name) {
|
||||
t.Errorf("%+q roundtripped through %+q to %+q", test.name, s, unescaped)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNameTrimSuffix(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name, suffix string
|
||||
trimmed string
|
||||
ok bool
|
||||
}{
|
||||
{"", "", ".", true},
|
||||
{".", ".", ".", true},
|
||||
{"abc", "", "abc", true},
|
||||
{"abc", ".", "abc", true},
|
||||
{"", "abc", ".", false},
|
||||
{".", "abc", ".", false},
|
||||
{"example.com", "com", "example", true},
|
||||
{"example.com", "net", ".", false},
|
||||
{"example.com", "example.com", ".", true},
|
||||
{"example.com", "test.com", ".", false},
|
||||
{"example.com", "xample.com", ".", false},
|
||||
{"example.com", "example", ".", false},
|
||||
{"example.com", "COM", "example", true},
|
||||
{"EXAMPLE.COM", "com", "EXAMPLE", true},
|
||||
} {
|
||||
tmp, ok := mustParseName(test.name).TrimSuffix(mustParseName(test.suffix))
|
||||
trimmed := tmp.String()
|
||||
if ok != test.ok || trimmed != test.trimmed {
|
||||
t.Errorf("TrimSuffix %+q %+q returned (%+q, %v), expected (%+q, %v)",
|
||||
test.name, test.suffix, trimmed, ok, test.trimmed, test.ok)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadName(t *testing.T) {
|
||||
// Good tests.
|
||||
for _, test := range []struct {
|
||||
start int64
|
||||
end int64
|
||||
input string
|
||||
s string
|
||||
}{
|
||||
// Empty name.
|
||||
{0, 1, "\x00abcd", "."},
|
||||
// No pointers.
|
||||
{12, 25, "AAAABBBBCCCC\x07example\x03com\x00", "example.com"},
|
||||
// Backward pointer.
|
||||
{25, 31, "AAAABBBBCCCC\x07example\x03com\x00\x03sub\xc0\x0c", "sub.example.com"},
|
||||
// Forward pointer.
|
||||
{0, 4, "\x01a\xc0\x04\x03bcd\x00", "a.bcd"},
|
||||
// Two backwards pointers.
|
||||
{31, 38, "AAAABBBBCCCC\x07example\x03com\x00\x03sub\xc0\x0c\x04sub2\xc0\x19", "sub2.sub.example.com"},
|
||||
// Forward then backward pointer.
|
||||
{25, 31, "AAAABBBBCCCC\x07example\x03com\x00\x03sub\xc0\x1f\x04sub2\xc0\x0c", "sub.sub2.example.com"},
|
||||
// Overlapping codons.
|
||||
{0, 4, "\x01a\xc0\x03bcd\x00", "a.bcd"},
|
||||
// Pointer to empty label.
|
||||
{0, 10, "\x07example\xc0\x0a\x00", "example"},
|
||||
{1, 11, "\x00\x07example\xc0\x00", "example"},
|
||||
// Pointer to pointer to empty label.
|
||||
{0, 10, "\x07example\xc0\x0a\xc0\x0c\x00", "example"},
|
||||
{1, 11, "\x00\x07example\xc0\x0c\xc0\x00", "example"},
|
||||
} {
|
||||
r := bytes.NewReader([]byte(test.input))
|
||||
_, err := r.Seek(test.start, io.SeekStart)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
name, err := readName(r)
|
||||
if err != nil {
|
||||
t.Errorf("%+q returned error %s", test.input, err)
|
||||
continue
|
||||
}
|
||||
s := name.String()
|
||||
if s != test.s {
|
||||
t.Errorf("%+q returned %+q, expected %+q", test.input, s, test.s)
|
||||
continue
|
||||
}
|
||||
cur, _ := r.Seek(0, io.SeekCurrent)
|
||||
if cur != test.end {
|
||||
t.Errorf("%+q left offset %d, expected %d", test.input, cur, test.end)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Bad tests.
|
||||
for _, test := range []struct {
|
||||
start int64
|
||||
input string
|
||||
err error
|
||||
}{
|
||||
{0, "", io.ErrUnexpectedEOF},
|
||||
// Reserved label type.
|
||||
{0, "\x80example", ErrReservedLabelType},
|
||||
// Reserved label type.
|
||||
{0, "\x40example", ErrReservedLabelType},
|
||||
// No Terminating empty label.
|
||||
{0, "\x07example\x03com", io.ErrUnexpectedEOF},
|
||||
// Pointer past end of buffer.
|
||||
{0, "\x07example\xc0\xff", io.ErrUnexpectedEOF},
|
||||
// Pointer to self.
|
||||
{0, "\x07example\x03com\xc0\x0c", ErrTooManyPointers},
|
||||
// Pointer to self with intermediate label.
|
||||
{0, "\x07example\x03com\xc0\x08", ErrTooManyPointers},
|
||||
// Two pointers that point to each other.
|
||||
{0, "\xc0\x02\xc0\x00", ErrTooManyPointers},
|
||||
// Two pointers that point to each other, with intermediate labels.
|
||||
{0, "\x01a\xc0\x04\x01b\xc0\x00", ErrTooManyPointers},
|
||||
// EOF while reading label.
|
||||
{0, "\x0aexample", io.ErrUnexpectedEOF},
|
||||
// EOF before second byte of pointer.
|
||||
{0, "\xc0", io.ErrUnexpectedEOF},
|
||||
{0, "\x07example\xc0", io.ErrUnexpectedEOF},
|
||||
} {
|
||||
r := bytes.NewReader([]byte(test.input))
|
||||
_, err := r.Seek(test.start, io.SeekStart)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
name, err := readName(r)
|
||||
if err == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
if err != test.err {
|
||||
t.Errorf("%+q returned (%+q, %v), expected %v", test.input, name, err, test.err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mustParseName(s string) Name {
|
||||
name, err := ParseName(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func questionsEqual(a, b *Question) bool {
|
||||
if !namesEqual(a.Name, b.Name) {
|
||||
return false
|
||||
}
|
||||
if a.Type != b.Type || a.Class != b.Class {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func rrsEqual(a, b *RR) bool {
|
||||
if !namesEqual(a.Name, b.Name) {
|
||||
return false
|
||||
}
|
||||
if a.Type != b.Type || a.Class != b.Class || a.TTL != b.TTL {
|
||||
return false
|
||||
}
|
||||
if !bytes.Equal(a.Data, b.Data) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func messagesEqual(a, b *Message) bool {
|
||||
if a.ID != b.ID || a.Flags != b.Flags {
|
||||
return false
|
||||
}
|
||||
if len(a.Question) != len(b.Question) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(a.Question); i++ {
|
||||
if !questionsEqual(&a.Question[i], &b.Question[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, rec := range []struct{ rrA, rrB []RR }{
|
||||
{a.Answer, b.Answer},
|
||||
{a.Authority, b.Authority},
|
||||
{a.Additional, b.Additional},
|
||||
} {
|
||||
if len(rec.rrA) != len(rec.rrB) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(rec.rrA); i++ {
|
||||
if !rrsEqual(&rec.rrA[i], &rec.rrB[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestMessageFromWireFormat(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
buf string
|
||||
expected Message
|
||||
err error
|
||||
}{
|
||||
{
|
||||
"\x12\x34",
|
||||
Message{},
|
||||
io.ErrUnexpectedEOF,
|
||||
},
|
||||
{
|
||||
"\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x07example\x03com\x00\x00\x01\x00\x01",
|
||||
Message{
|
||||
ID: 0x1234,
|
||||
Flags: 0x0100,
|
||||
Question: []Question{
|
||||
{
|
||||
Name: mustParseName("www.example.com"),
|
||||
Type: 1,
|
||||
Class: 1,
|
||||
},
|
||||
},
|
||||
Answer: []RR{},
|
||||
Authority: []RR{},
|
||||
Additional: []RR{},
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x07example\x03com\x00\x00\x01\x00\x01X",
|
||||
Message{},
|
||||
ErrTrailingBytes,
|
||||
},
|
||||
{
|
||||
"\x12\x34\x81\x80\x00\x01\x00\x01\x00\x00\x00\x00\x03www\x07example\x03com\x00\x00\x01\x00\x01\x03www\x07example\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x80\x00\x04\xc0\x00\x02\x01",
|
||||
Message{
|
||||
ID: 0x1234,
|
||||
Flags: 0x8180,
|
||||
Question: []Question{
|
||||
{
|
||||
Name: mustParseName("www.example.com"),
|
||||
Type: 1,
|
||||
Class: 1,
|
||||
},
|
||||
},
|
||||
Answer: []RR{
|
||||
{
|
||||
Name: mustParseName("www.example.com"),
|
||||
Type: 1,
|
||||
Class: 1,
|
||||
TTL: 128,
|
||||
Data: []byte{192, 0, 2, 1},
|
||||
},
|
||||
},
|
||||
Authority: []RR{},
|
||||
Additional: []RR{},
|
||||
},
|
||||
nil,
|
||||
},
|
||||
} {
|
||||
message, err := MessageFromWireFormat([]byte(test.buf))
|
||||
if err != test.err || (err == nil && !messagesEqual(&message, &test.expected)) {
|
||||
t.Errorf("%+q\nreturned (%+v, %v)\nexpected (%+v, %v)",
|
||||
test.buf, message, err, test.expected, test.err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageWireFormatRoundTrip(t *testing.T) {
|
||||
for _, message := range []Message{
|
||||
{
|
||||
ID: 0x1234,
|
||||
Flags: 0x0100,
|
||||
Question: []Question{
|
||||
{
|
||||
Name: mustParseName("www.example.com"),
|
||||
Type: 1,
|
||||
Class: 1,
|
||||
},
|
||||
{
|
||||
Name: mustParseName("www2.example.com"),
|
||||
Type: 2,
|
||||
Class: 2,
|
||||
},
|
||||
},
|
||||
Answer: []RR{
|
||||
{
|
||||
Name: mustParseName("abc"),
|
||||
Type: 2,
|
||||
Class: 3,
|
||||
TTL: 0xffffffff,
|
||||
Data: []byte{1},
|
||||
},
|
||||
{
|
||||
Name: mustParseName("xyz"),
|
||||
Type: 2,
|
||||
Class: 3,
|
||||
TTL: 255,
|
||||
Data: []byte{},
|
||||
},
|
||||
},
|
||||
Authority: []RR{
|
||||
{
|
||||
Name: mustParseName("."),
|
||||
Type: 65535,
|
||||
Class: 65535,
|
||||
TTL: 0,
|
||||
Data: []byte("XXXXXXXXXXXXXXXXXXX"),
|
||||
},
|
||||
},
|
||||
Additional: []RR{},
|
||||
},
|
||||
} {
|
||||
buf, err := message.WireFormat()
|
||||
if err != nil {
|
||||
t.Errorf("%+v cannot make wire format: %v", message, err)
|
||||
continue
|
||||
}
|
||||
message2, err := MessageFromWireFormat(buf)
|
||||
if err != nil {
|
||||
t.Errorf("%+q cannot parse wire format: %v", buf, err)
|
||||
continue
|
||||
}
|
||||
if !messagesEqual(&message, &message2) {
|
||||
t.Errorf("messages unequal\nbefore: %+v\n after: %+v", message, message2)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeRDataTXT(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
p []byte
|
||||
decoded []byte
|
||||
err error
|
||||
}{
|
||||
{[]byte{}, nil, io.ErrUnexpectedEOF},
|
||||
{[]byte("\x00"), []byte{}, nil},
|
||||
{[]byte("\x01"), nil, io.ErrUnexpectedEOF},
|
||||
} {
|
||||
decoded, err := DecodeRDataTXT(test.p)
|
||||
if err != test.err || (err == nil && !bytes.Equal(decoded, test.decoded)) {
|
||||
t.Errorf("%+q\nreturned (%+q, %v)\nexpected (%+q, %v)",
|
||||
test.p, decoded, err, test.decoded, test.err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeRDataTXT(t *testing.T) {
|
||||
// Encoding 0 bytes needs to return at least a single length octet of
|
||||
// zero, not an empty slice.
|
||||
p := make([]byte, 0)
|
||||
encoded := EncodeRDataTXT(p)
|
||||
if len(encoded) < 0 {
|
||||
t.Errorf("EncodeRDataTXT(%v) returned %v", p, encoded)
|
||||
}
|
||||
|
||||
// 255 bytes should be able to be encoded into 256 bytes.
|
||||
p = make([]byte, 255)
|
||||
encoded = EncodeRDataTXT(p)
|
||||
if len(encoded) > 256 {
|
||||
t.Errorf("EncodeRDataTXT(%d bytes) returned %d bytes", len(p), len(encoded))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRDataTXTRoundTrip(t *testing.T) {
|
||||
for _, p := range [][]byte{
|
||||
{},
|
||||
[]byte("\x00"),
|
||||
{
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
|
||||
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
|
||||
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
|
||||
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
|
||||
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
|
||||
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
|
||||
0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f,
|
||||
0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f,
|
||||
0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f,
|
||||
0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f,
|
||||
0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf,
|
||||
0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf,
|
||||
0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf,
|
||||
0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf,
|
||||
0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef,
|
||||
0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff,
|
||||
},
|
||||
} {
|
||||
rdata := EncodeRDataTXT(p)
|
||||
decoded, err := DecodeRDataTXT(rdata)
|
||||
if err != nil || !bytes.Equal(decoded, p) {
|
||||
t.Errorf("%+q returned (%+q, %v)", p, decoded, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
23
internal/dnsttcore/dns/fuzz.go
Normal file
23
internal/dnsttcore/dns/fuzz.go
Normal file
@@ -0,0 +1,23 @@
|
||||
//go:build gofuzz
|
||||
// +build gofuzz
|
||||
|
||||
// Fuzzing driver for https://github.com/dvyukov/go-fuzz.
|
||||
// go get -u github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build
|
||||
// $GOPATH/bin/go-fuzz-build
|
||||
// $GOPATH/bin/go-fuzz
|
||||
//
|
||||
// Related link: https://blog.cloudflare.com/dns-parser-meet-go-fuzzer/
|
||||
|
||||
package dns
|
||||
|
||||
func Fuzz(data []byte) int {
|
||||
msg, err := MessageFromWireFormat(data)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
_, err = msg.WireFormat()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return 1 // prioritize this input
|
||||
}
|
||||
276
internal/dnsttcore/noise/noise.go
Normal file
276
internal/dnsttcore/noise/noise.go
Normal file
@@ -0,0 +1,276 @@
|
||||
// Package noise provides a net.Conn-like interface for a
|
||||
// Noise_NK_25519_ChaChaPoly_BLAKE2s. It encodes Noise messages onto a reliable
|
||||
// stream using 16-bit length prefixes.
|
||||
//
|
||||
// https://noiseprotocol.org/noise.html
|
||||
package noise
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/flynn/noise"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
|
||||
// The length of public and private keys as returned by GeneratePrivkey.
|
||||
const KeyLen = 32
|
||||
|
||||
// cipherSuite represents 25519_ChaChaPoly_BLAKE2s.
|
||||
var cipherSuite = noise.NewCipherSuite(noise.DH25519, noise.CipherChaChaPoly, noise.HashBLAKE2s)
|
||||
|
||||
// readMessage reads a length-prefixed message from r. It returns a nil error
|
||||
// only when a complete message was read. It returns io.EOF only when there were
|
||||
// 0 bytes remaining to read from r. It returns io.ErrUnexpectedEOF when EOF
|
||||
// occurs in the middle of an encoded message.
|
||||
func readMessage(r io.Reader) ([]byte, error) {
|
||||
var length uint16
|
||||
err := binary.Read(r, binary.BigEndian, &length)
|
||||
if err != nil {
|
||||
// We may return a real io.EOF only here.
|
||||
return nil, err
|
||||
}
|
||||
msg := make([]byte, int(length))
|
||||
_, err = io.ReadFull(r, msg)
|
||||
// Here we must change io.EOF to io.ErrUnexpectedEOF.
|
||||
if err == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return msg, err
|
||||
}
|
||||
|
||||
// writeMessage writes msg as a length-prefixed message to w. It panics if the
|
||||
// length of msg cannot be represented in 16 bits.
|
||||
func writeMessage(w io.Writer, msg []byte) error {
|
||||
length := uint16(len(msg))
|
||||
if int(length) != len(msg) {
|
||||
panic(len(msg))
|
||||
}
|
||||
err := binary.Write(w, binary.BigEndian, length)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(msg)
|
||||
return err
|
||||
}
|
||||
|
||||
// socket is the internal type that represents a Noise-wrapped
|
||||
// io.ReadWriteCloser.
|
||||
type socket struct {
|
||||
recvPipe *io.PipeReader
|
||||
sendCipher *noise.CipherState
|
||||
io.ReadWriteCloser
|
||||
}
|
||||
|
||||
func newSocket(rwc io.ReadWriteCloser, recvCipher, sendCipher *noise.CipherState) *socket {
|
||||
pr, pw := io.Pipe()
|
||||
// This loop calls readMessage, decrypts the messages, and feeds them
|
||||
// into recvPipe where they will be returned from Read.
|
||||
go func() (err error) {
|
||||
defer func() {
|
||||
pw.CloseWithError(err)
|
||||
}()
|
||||
for {
|
||||
msg, err := readMessage(rwc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p, err := recvCipher.Decrypt(nil, nil, msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = pw.Write(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}()
|
||||
return &socket{
|
||||
sendCipher: sendCipher,
|
||||
recvPipe: pr,
|
||||
ReadWriteCloser: rwc,
|
||||
}
|
||||
}
|
||||
|
||||
// Read reads decrypted data from the wrapped io.Reader.
|
||||
func (s *socket) Read(p []byte) (int, error) {
|
||||
return s.recvPipe.Read(p)
|
||||
}
|
||||
|
||||
// Write writes encrypted data from the wrapped io.Writer.
|
||||
func (s *socket) Write(p []byte) (int, error) {
|
||||
total := 0
|
||||
for len(p) > 0 {
|
||||
n := len(p)
|
||||
if n > 4096 {
|
||||
n = 4096
|
||||
}
|
||||
msg, err := s.sendCipher.Encrypt(nil, nil, p[:n])
|
||||
if err != nil {
|
||||
return total, err
|
||||
}
|
||||
err = writeMessage(s.ReadWriteCloser, msg)
|
||||
if err != nil {
|
||||
return total, err
|
||||
}
|
||||
total += n
|
||||
p = p[n:]
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// newConfig instantiates configuration settings that are common to clients and
|
||||
// servers.
|
||||
func newConfig() noise.Config {
|
||||
return noise.Config{
|
||||
CipherSuite: cipherSuite,
|
||||
Pattern: noise.HandshakeNK,
|
||||
Prologue: []byte("dnstt 2020-04-13"),
|
||||
}
|
||||
}
|
||||
|
||||
// NewClient wraps an io.ReadWriteCloser in a Noise protocol as a client, and
|
||||
// returns after completing the handshake. It returns a non-nil error if there
|
||||
// is an error during the handshake.
|
||||
func NewClient(rwc io.ReadWriteCloser, serverPubkey []byte) (io.ReadWriteCloser, error) {
|
||||
config := newConfig()
|
||||
config.Initiator = true
|
||||
config.PeerStatic = serverPubkey
|
||||
handshakeState, err := noise.NewHandshakeState(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// -> e, es
|
||||
msg, _, _, err := handshakeState.WriteMessage(nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = writeMessage(rwc, msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// <- e, es
|
||||
msg, err = readMessage(rwc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload, sendCipher, recvCipher, err := handshakeState.ReadMessage(nil, msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(payload) != 0 {
|
||||
return nil, errors.New("unexpected server payload")
|
||||
}
|
||||
|
||||
return newSocket(rwc, recvCipher, sendCipher), nil
|
||||
}
|
||||
|
||||
// NewClient wraps an io.ReadWriteCloser in a Noise protocol as a server, and
|
||||
// returns after completing the handshake. It returns a non-nil error if there
|
||||
// is an error during the handshake.
|
||||
func NewServer(rwc io.ReadWriteCloser, serverPrivkey []byte) (io.ReadWriteCloser, error) {
|
||||
config := newConfig()
|
||||
config.Initiator = false
|
||||
config.StaticKeypair = noise.DHKey{
|
||||
Private: serverPrivkey,
|
||||
Public: PubkeyFromPrivkey(serverPrivkey),
|
||||
}
|
||||
handshakeState, err := noise.NewHandshakeState(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// -> e, es
|
||||
msg, err := readMessage(rwc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload, _, _, err := handshakeState.ReadMessage(nil, msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(payload) != 0 {
|
||||
return nil, errors.New("unexpected client payload")
|
||||
}
|
||||
|
||||
// <- e, es
|
||||
msg, recvCipher, sendCipher, err := handshakeState.WriteMessage(nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = writeMessage(rwc, msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newSocket(rwc, recvCipher, sendCipher), nil
|
||||
}
|
||||
|
||||
// GeneratePrivkey generates a private key. The corresponding public key can be
|
||||
// derived using PubkeyFromPrivkey.
|
||||
func GeneratePrivkey() ([]byte, error) {
|
||||
pair, err := noise.DH25519.GenerateKeypair(rand.Reader)
|
||||
return pair.Private, err
|
||||
}
|
||||
|
||||
// PubkeyFromPrivkey returns the public key that corresponds to privkey.
|
||||
func PubkeyFromPrivkey(privkey []byte) []byte {
|
||||
pubkey, err := curve25519.X25519(privkey, curve25519.Basepoint)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return pubkey
|
||||
}
|
||||
|
||||
// ReadKey reads a hex-encoded key from r. r must consist of a single line, with
|
||||
// or without a '\n' line terminator. The line must consist of KeyLen
|
||||
// hex-encoded bytes.
|
||||
func ReadKey(r io.Reader) ([]byte, error) {
|
||||
br := bufio.NewReader(io.LimitReader(r, 100))
|
||||
line, err := br.ReadString('\n')
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
if err == nil {
|
||||
// Check that we're at EOF.
|
||||
_, err = br.ReadByte()
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
} else if err == nil {
|
||||
err = fmt.Errorf("file contains more than one line")
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
line = strings.TrimSuffix(line, "\n")
|
||||
return DecodeKey(line)
|
||||
}
|
||||
|
||||
// WriteKey writes the hex-encoded key in a single line to w.
|
||||
func WriteKey(w io.Writer, key []byte) error {
|
||||
_, err := fmt.Fprintf(w, "%x\n", key)
|
||||
return err
|
||||
}
|
||||
|
||||
// DecodeKey decodes a hex-encoded private or public key.
|
||||
func DecodeKey(s string) ([]byte, error) {
|
||||
key, err := hex.DecodeString(s)
|
||||
if err == nil && len(key) != KeyLen {
|
||||
err = fmt.Errorf("length is %d, expected %d", len(key), KeyLen)
|
||||
}
|
||||
return key, err
|
||||
}
|
||||
|
||||
// EncodeKey encodes a hex-encoded private or public key.
|
||||
func EncodeKey(key []byte) string {
|
||||
return hex.EncodeToString(key)
|
||||
}
|
||||
218
internal/dnsttcore/noise/noise_test.go
Normal file
218
internal/dnsttcore/noise/noise_test.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package noise
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/flynn/noise"
|
||||
)
|
||||
|
||||
func allMessages(buf []byte) ([][]byte, error) {
|
||||
var messages [][]byte
|
||||
r := bytes.NewReader(buf)
|
||||
for {
|
||||
msg, err := readMessage(r)
|
||||
if err != nil {
|
||||
return messages, err
|
||||
}
|
||||
messages = append(messages, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func messagesEqual(a, b [][]byte) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if !bytes.Equal(a[i], b[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestReadMessage(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
input string
|
||||
messages [][]byte
|
||||
err error
|
||||
}{
|
||||
{"", [][]byte{}, io.EOF},
|
||||
{"\x00", [][]byte{}, io.ErrUnexpectedEOF},
|
||||
{"\x00\x00", [][]byte{{}}, io.EOF},
|
||||
{"\x00\x00\x00", [][]byte{{}}, io.ErrUnexpectedEOF},
|
||||
{"\x00\x01", [][]byte{}, io.ErrUnexpectedEOF},
|
||||
{"\x00\x05hello\x00\x05world", [][]byte{[]byte("hello"), []byte("world")}, io.EOF},
|
||||
} {
|
||||
packets, err := allMessages([]byte(test.input))
|
||||
if !messagesEqual(packets, test.messages) || err != test.err {
|
||||
t.Errorf("%x\nreturned %x %v\nexpected %x %v",
|
||||
test.input, packets, err, test.messages, test.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageRoundTrip(t *testing.T) {
|
||||
for _, messages := range [][][]byte{
|
||||
{},
|
||||
} {
|
||||
var buf bytes.Buffer
|
||||
for _, msg := range messages {
|
||||
err := writeMessage(&buf, msg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
output, err := allMessages(buf.Bytes())
|
||||
if !messagesEqual(output, messages) || err != io.EOF {
|
||||
t.Errorf("%x roundtripped to %x %v",
|
||||
messages, output, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadKey(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
input string
|
||||
output []byte
|
||||
}{
|
||||
{"", nil},
|
||||
{"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde", nil},
|
||||
{"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", []byte("\x01\x23\x45\x67\x89\xab\xcd\xef\x01\x23\x45\x67\x89\xab\xcd\xef\x01\x23\x45\x67\x89\xab\xcd\xef\x01\x23\x45\x67\x89\xab\xcd\xef")},
|
||||
{"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n", []byte("\x01\x23\x45\x67\x89\xab\xcd\xef\x01\x23\x45\x67\x89\xab\xcd\xef\x01\x23\x45\x67\x89\xab\xcd\xef\x01\x23\x45\x67\x89\xab\xcd\xef")},
|
||||
{"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0", nil},
|
||||
{"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\nX", nil},
|
||||
{"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n\n", nil},
|
||||
{"\n0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", nil},
|
||||
{"X123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", nil},
|
||||
{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil},
|
||||
} {
|
||||
output, err := ReadKey(bytes.NewReader([]byte(test.input)))
|
||||
if test.output == nil {
|
||||
if err == nil {
|
||||
t.Errorf("%+q expected error", test.input)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("%+q returned error %v", test.input, err)
|
||||
} else if !bytes.Equal(output, test.output) {
|
||||
t.Errorf("%+q got %x, expected %x", test.input, output, test.output)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnexpectedPayload(t *testing.T) {
|
||||
privkey, err := GeneratePrivkey()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
pubkey := PubkeyFromPrivkey(privkey)
|
||||
|
||||
// Test the client sending an unexpected payload.
|
||||
clientWithPayload := func(rwc io.ReadWriteCloser) error {
|
||||
config := newConfig()
|
||||
config.Initiator = true
|
||||
config.PeerStatic = pubkey
|
||||
handshakeState, err := noise.NewHandshakeState(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// -> e, es
|
||||
msg, _, _, err := handshakeState.WriteMessage(nil, []byte("payload"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = writeMessage(rwc, msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// <- e, es
|
||||
// Return nil for all errors after this point, because we expect
|
||||
// the server to have failed, but we want to keep up the game
|
||||
// just in case the server did not fail.
|
||||
msg, err = readMessage(rwc)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
_, _, _, err = handshakeState.ReadMessage(nil, msg)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func() {
|
||||
c, s := net.Pipe()
|
||||
defer s.Close()
|
||||
|
||||
// Fake a client side that sends a payload.
|
||||
go func() {
|
||||
defer c.Close()
|
||||
err := clientWithPayload(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
server, err := NewServer(s, privkey)
|
||||
if err == nil || err.Error() != "unexpected client payload" || server != nil {
|
||||
t.Errorf("NewServer got (%T, %v)", server, err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Test the server sending an unexpected payload.
|
||||
serverWithPayload := func(rwc io.ReadWriteCloser) error {
|
||||
config := newConfig()
|
||||
config.Initiator = false
|
||||
config.StaticKeypair = noise.DHKey{Private: privkey, Public: pubkey}
|
||||
handshakeState, err := noise.NewHandshakeState(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// -> e, es
|
||||
msg, err := readMessage(rwc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, _, err = handshakeState.ReadMessage(nil, msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// <- e, es
|
||||
msg, _, _, err = handshakeState.WriteMessage(nil, []byte("payload"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = writeMessage(rwc, msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func() {
|
||||
c, s := net.Pipe()
|
||||
defer c.Close()
|
||||
|
||||
// Fake a server side that sends a payload.
|
||||
go func() {
|
||||
defer s.Close()
|
||||
err := serverWithPayload(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
client, err := NewClient(c, pubkey)
|
||||
if err == nil || err.Error() != "unexpected server payload" || client != nil {
|
||||
t.Errorf("NewClient got (%T, %v)", client, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
28
internal/dnsttcore/turbotunnel/clientid.go
Normal file
28
internal/dnsttcore/turbotunnel/clientid.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package turbotunnel
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// ClientID is an abstract identifier that binds together all the communications
|
||||
// belonging to a single client session, even though those communications may
|
||||
// arrive from multiple IP addresses or over multiple lower-level connections.
|
||||
// It plays the same role that an (IP address, port number) tuple plays in a
|
||||
// net.UDPConn: it's the return address pertaining to a long-lived abstract
|
||||
// client session. The client attaches its ClientID to each of its
|
||||
// communications, enabling the server to disambiguate requests among its many
|
||||
// clients. ClientID implements the net.Addr interface.
|
||||
type ClientID [8]byte
|
||||
|
||||
func NewClientID() ClientID {
|
||||
var id ClientID
|
||||
_, err := rand.Read(id[:])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func (id ClientID) Network() string { return "clientid" }
|
||||
func (id ClientID) String() string { return hex.EncodeToString(id[:]) }
|
||||
22
internal/dnsttcore/turbotunnel/consts.go
Normal file
22
internal/dnsttcore/turbotunnel/consts.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Package turbotunnel is facilities for embedding packet-based reliability
|
||||
// protocols inside other protocols.
|
||||
//
|
||||
// https://github.com/net4people/bbs/issues/9
|
||||
package turbotunnel
|
||||
|
||||
import "errors"
|
||||
|
||||
// QueueSize is the size of send and receive queues in QueuePacketConn and
|
||||
// RemoteMap.
|
||||
const QueueSize = 128
|
||||
|
||||
var errClosedPacketConn = errors.New("operation on closed connection")
|
||||
var errNotImplemented = errors.New("not implemented")
|
||||
|
||||
// DummyAddr is a placeholder net.Addr, for when a programming interface
|
||||
// requires a net.Addr but there is none relevant. All DummyAddrs compare equal
|
||||
// to each other.
|
||||
type DummyAddr struct{}
|
||||
|
||||
func (addr DummyAddr) Network() string { return "dummy" }
|
||||
func (addr DummyAddr) String() string { return "dummy" }
|
||||
162
internal/dnsttcore/turbotunnel/queuepacketconn.go
Normal file
162
internal/dnsttcore/turbotunnel/queuepacketconn.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package turbotunnel
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// taggedPacket is a combination of a []byte and a net.Addr, encapsulating the
|
||||
// return type of PacketConn.ReadFrom.
|
||||
type taggedPacket struct {
|
||||
P []byte
|
||||
Addr net.Addr
|
||||
}
|
||||
|
||||
// QueuePacketConn implements net.PacketConn by storing queues of packets. There
|
||||
// is one incoming queue (where packets are additionally tagged by the source
|
||||
// address of the peer that sent them). There are many outgoing queues, one for
|
||||
// each remote peer address that has been recently seen. The QueueIncoming
|
||||
// method inserts a packet into the incoming queue, to eventually be returned by
|
||||
// ReadFrom. WriteTo inserts a packet into an address-specific outgoing queue,
|
||||
// which can later by accessed through the OutgoingQueue method.
|
||||
//
|
||||
// Besides the outgoing queues, there is also a one-element "stash" for each
|
||||
// remote peer address. You can stash a packet using the Stash method, and get
|
||||
// it back later by receiving from the channel returned by Unstash. The stash is
|
||||
// meant as a convenient place to temporarily store a single packet, such as
|
||||
// when you've read one too many packets from the send queue and need to store
|
||||
// the extra packet to be processed first in the next pass. It's the caller's
|
||||
// responsibility to Unstash what they have Stashed. Calling Stash does not put
|
||||
// the packet at the head of the send queue; if there is the possibility that a
|
||||
// packet has been stashed, it must be checked for by calling Unstash in
|
||||
// addition to OutgoingQueue.
|
||||
type QueuePacketConn struct {
|
||||
remotes *RemoteMap
|
||||
localAddr net.Addr
|
||||
recvQueue chan taggedPacket
|
||||
closeOnce sync.Once
|
||||
closed chan struct{}
|
||||
// What error to return when the QueuePacketConn is closed.
|
||||
err atomic.Value
|
||||
}
|
||||
|
||||
// NewQueuePacketConn makes a new QueuePacketConn, set to track recent peers
|
||||
// for at least a duration of timeout.
|
||||
func NewQueuePacketConn(localAddr net.Addr, timeout time.Duration) *QueuePacketConn {
|
||||
return &QueuePacketConn{
|
||||
remotes: NewRemoteMap(timeout),
|
||||
localAddr: localAddr,
|
||||
recvQueue: make(chan taggedPacket, QueueSize),
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// QueueIncoming queues and incoming packet and its source address, to be
|
||||
// returned in a future call to ReadFrom.
|
||||
func (c *QueuePacketConn) QueueIncoming(p []byte, addr net.Addr) {
|
||||
select {
|
||||
case <-c.closed:
|
||||
// If we're closed, silently drop it.
|
||||
return
|
||||
default:
|
||||
}
|
||||
// Copy the slice so that the caller may reuse it.
|
||||
buf := make([]byte, len(p))
|
||||
copy(buf, p)
|
||||
select {
|
||||
case c.recvQueue <- taggedPacket{buf, addr}:
|
||||
default:
|
||||
// Drop the incoming packet if the receive queue is full.
|
||||
}
|
||||
}
|
||||
|
||||
// OutgoingQueue returns the queue of outgoing packets corresponding to addr,
|
||||
// creating it if necessary. The contents of the queue will be packets that are
|
||||
// written to the address in question using WriteTo.
|
||||
func (c *QueuePacketConn) OutgoingQueue(addr net.Addr) <-chan []byte {
|
||||
return c.remotes.SendQueue(addr)
|
||||
}
|
||||
|
||||
// Stash places p in the stash for addr, if the stash is not already occupied.
|
||||
// Returns true if the packet was placed in the stash, or false if the stash was
|
||||
// already occupied. This method is similar to WriteTo, except that it puts the
|
||||
// packet in the stash queue (accessible via Unstash), rather than the outgoing
|
||||
// queue (accessible via OutgoingQueue).
|
||||
func (c *QueuePacketConn) Stash(p []byte, addr net.Addr) bool {
|
||||
return c.remotes.Stash(addr, p)
|
||||
}
|
||||
|
||||
// Unstash returns the channel that represents the stash for addr.
|
||||
func (c *QueuePacketConn) Unstash(addr net.Addr) <-chan []byte {
|
||||
return c.remotes.Unstash(addr)
|
||||
}
|
||||
|
||||
// ReadFrom returns a packet and address previously stored by QueueIncoming.
|
||||
func (c *QueuePacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
|
||||
select {
|
||||
case <-c.closed:
|
||||
return 0, nil, &net.OpError{Op: "read", Net: c.LocalAddr().Network(), Addr: c.LocalAddr(), Err: c.err.Load().(error)}
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case <-c.closed:
|
||||
return 0, nil, &net.OpError{Op: "read", Net: c.LocalAddr().Network(), Addr: c.LocalAddr(), Err: c.err.Load().(error)}
|
||||
case packet := <-c.recvQueue:
|
||||
return copy(p, packet.P), packet.Addr, nil
|
||||
}
|
||||
}
|
||||
|
||||
// WriteTo queues an outgoing packet for the given address. The queue can later
|
||||
// be retrieved using the OutgoingQueue method.
|
||||
func (c *QueuePacketConn) WriteTo(p []byte, addr net.Addr) (int, error) {
|
||||
select {
|
||||
case <-c.closed:
|
||||
return 0, &net.OpError{Op: "write", Net: c.LocalAddr().Network(), Addr: c.LocalAddr(), Err: c.err.Load().(error)}
|
||||
default:
|
||||
}
|
||||
// Copy the slice so that the caller may reuse it.
|
||||
buf := make([]byte, len(p))
|
||||
copy(buf, p)
|
||||
select {
|
||||
case c.remotes.SendQueue(addr) <- buf:
|
||||
return len(buf), nil
|
||||
default:
|
||||
// Drop the outgoing packet if the send queue is full.
|
||||
return len(buf), nil
|
||||
}
|
||||
}
|
||||
|
||||
// closeWithError unblocks pending operations and makes future operations fail
|
||||
// with the given error. If err is nil, it becomes errClosedPacketConn.
|
||||
func (c *QueuePacketConn) closeWithError(err error) error {
|
||||
var newlyClosed bool
|
||||
c.closeOnce.Do(func() {
|
||||
newlyClosed = true
|
||||
// Store the error to be returned by future PacketConn
|
||||
// operations.
|
||||
if err == nil {
|
||||
err = errClosedPacketConn
|
||||
}
|
||||
c.err.Store(err)
|
||||
close(c.closed)
|
||||
})
|
||||
if !newlyClosed {
|
||||
return &net.OpError{Op: "close", Net: c.LocalAddr().Network(), Addr: c.LocalAddr(), Err: c.err.Load().(error)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close unblocks pending operations and makes future operations fail with a
|
||||
// "closed connection" error.
|
||||
func (c *QueuePacketConn) Close() error {
|
||||
return c.closeWithError(nil)
|
||||
}
|
||||
|
||||
// LocalAddr returns the localAddr value that was passed to NewQueuePacketConn.
|
||||
func (c *QueuePacketConn) LocalAddr() net.Addr { return c.localAddr }
|
||||
|
||||
func (c *QueuePacketConn) SetDeadline(t time.Time) error { return errNotImplemented }
|
||||
func (c *QueuePacketConn) SetReadDeadline(t time.Time) error { return errNotImplemented }
|
||||
func (c *QueuePacketConn) SetWriteDeadline(t time.Time) error { return errNotImplemented }
|
||||
177
internal/dnsttcore/turbotunnel/remotemap.go
Normal file
177
internal/dnsttcore/turbotunnel/remotemap.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package turbotunnel
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// remoteRecord is a record of a recently seen remote peer, with the time it was
|
||||
// last seen and queues of outgoing packets.
|
||||
type remoteRecord struct {
|
||||
Addr net.Addr
|
||||
LastSeen time.Time
|
||||
SendQueue chan []byte
|
||||
Stash chan []byte
|
||||
}
|
||||
|
||||
// RemoteMap manages a mapping of live remote peers, keyed by address, to their
|
||||
// respective send queues. Each peer has two queues: a primary send queue, and a
|
||||
// "stash". The primary send queue is returned by the SendQueue method. The
|
||||
// stash is an auxiliary one-element queue accessed using the Stash and Unstash
|
||||
// methods. The stash is meant for use by callers that need to "unread" a packet
|
||||
// that's already been removed from the primary send queue.
|
||||
//
|
||||
// RemoteMap's functions are safe to call from multiple goroutines.
|
||||
type RemoteMap struct {
|
||||
// We use an inner structure to avoid exposing public heap.Interface
|
||||
// functions to users of remoteMap.
|
||||
inner remoteMapInner
|
||||
// Synchronizes access to inner.
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// NewRemoteMap creates a RemoteMap that expires peers after a timeout.
|
||||
//
|
||||
// If the timeout is 0, peers never expire.
|
||||
//
|
||||
// The timeout does not have to be kept in sync with smux's idle timeout. If a
|
||||
// peer is removed from the map while the smux session is still live, the worst
|
||||
// that can happen is a loss of whatever packets were in the send queue at the
|
||||
// time. If smux later decides to send more packets to the same peer, we'll
|
||||
// instantiate a new send queue, and if the peer is ever seen again with a
|
||||
// matching address, we'll deliver them.
|
||||
func NewRemoteMap(timeout time.Duration) *RemoteMap {
|
||||
m := &RemoteMap{
|
||||
inner: remoteMapInner{
|
||||
byAge: make([]*remoteRecord, 0),
|
||||
byAddr: make(map[net.Addr]int),
|
||||
},
|
||||
}
|
||||
if timeout > 0 {
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(timeout / 2)
|
||||
now := time.Now()
|
||||
m.lock.Lock()
|
||||
m.inner.removeExpired(now, timeout)
|
||||
m.lock.Unlock()
|
||||
}
|
||||
}()
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// SendQueue returns the send queue corresponding to addr, creating it if
|
||||
// necessary.
|
||||
func (m *RemoteMap) SendQueue(addr net.Addr) chan []byte {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
return m.inner.Lookup(addr, time.Now()).SendQueue
|
||||
}
|
||||
|
||||
// Stash places p in the stash corresponding to addr, if the stash is not
|
||||
// already occupied. Returns true if the p was placed in the stash, false
|
||||
// otherwise.
|
||||
func (m *RemoteMap) Stash(addr net.Addr, p []byte) bool {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
select {
|
||||
case m.inner.Lookup(addr, time.Now()).Stash <- p:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Unstash returns the channel that reads from the stash for addr.
|
||||
func (m *RemoteMap) Unstash(addr net.Addr) <-chan []byte {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
return m.inner.Lookup(addr, time.Now()).Stash
|
||||
}
|
||||
|
||||
// remoteMapInner is the inner type of RemoteMap, implementing heap.Interface.
|
||||
// byAge is the backing store, a heap ordered by LastSeen time, to facilitate
|
||||
// expiring old records. byAddr is a map from addresses to heap indices, to
|
||||
// allow looking up by address. Unlike RemoteMap, remoteMapInner requires
|
||||
// external synchonization.
|
||||
type remoteMapInner struct {
|
||||
byAge []*remoteRecord
|
||||
byAddr map[net.Addr]int
|
||||
}
|
||||
|
||||
// removeExpired removes all records whose LastSeen timestamp is more than
|
||||
// timeout in the past.
|
||||
func (inner *remoteMapInner) removeExpired(now time.Time, timeout time.Duration) {
|
||||
for len(inner.byAge) > 0 && now.Sub(inner.byAge[0].LastSeen) >= timeout {
|
||||
record := heap.Pop(inner).(*remoteRecord)
|
||||
close(record.SendQueue)
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup finds the existing record corresponding to addr, or creates a new
|
||||
// one if none exists yet. It updates the record's LastSeen time and returns the
|
||||
// record.
|
||||
func (inner *remoteMapInner) Lookup(addr net.Addr, now time.Time) *remoteRecord {
|
||||
var record *remoteRecord
|
||||
i, ok := inner.byAddr[addr]
|
||||
if ok {
|
||||
// Found one, update its LastSeen.
|
||||
record = inner.byAge[i]
|
||||
record.LastSeen = now
|
||||
heap.Fix(inner, i)
|
||||
} else {
|
||||
// Not found, create a new one.
|
||||
record = &remoteRecord{
|
||||
Addr: addr,
|
||||
LastSeen: now,
|
||||
SendQueue: make(chan []byte, QueueSize),
|
||||
Stash: make(chan []byte, 1),
|
||||
}
|
||||
heap.Push(inner, record)
|
||||
}
|
||||
return record
|
||||
}
|
||||
|
||||
// heap.Interface for remoteMapInner.
|
||||
|
||||
func (inner *remoteMapInner) Len() int {
|
||||
if len(inner.byAge) != len(inner.byAddr) {
|
||||
panic("inconsistent remoteMap")
|
||||
}
|
||||
return len(inner.byAge)
|
||||
}
|
||||
|
||||
func (inner *remoteMapInner) Less(i, j int) bool {
|
||||
return inner.byAge[i].LastSeen.Before(inner.byAge[j].LastSeen)
|
||||
}
|
||||
|
||||
func (inner *remoteMapInner) Swap(i, j int) {
|
||||
inner.byAge[i], inner.byAge[j] = inner.byAge[j], inner.byAge[i]
|
||||
inner.byAddr[inner.byAge[i].Addr] = i
|
||||
inner.byAddr[inner.byAge[j].Addr] = j
|
||||
}
|
||||
|
||||
func (inner *remoteMapInner) Push(x interface{}) {
|
||||
record := x.(*remoteRecord)
|
||||
if _, ok := inner.byAddr[record.Addr]; ok {
|
||||
panic("duplicate address in remoteMap")
|
||||
}
|
||||
// Insert into byAddr map.
|
||||
inner.byAddr[record.Addr] = len(inner.byAge)
|
||||
// Insert into byAge slice.
|
||||
inner.byAge = append(inner.byAge, record)
|
||||
}
|
||||
|
||||
func (inner *remoteMapInner) Pop() interface{} {
|
||||
n := len(inner.byAddr)
|
||||
// Remove from byAge slice.
|
||||
record := inner.byAge[n-1]
|
||||
inner.byAge[n-1] = nil
|
||||
inner.byAge = inner.byAge[:n-1]
|
||||
// Remove from byAddr map.
|
||||
delete(inner.byAddr, record.Addr)
|
||||
return record
|
||||
}
|
||||
45
internal/engine/logger.go
Normal file
45
internal/engine/logger.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LogEntry struct {
|
||||
ID int64 `json:"id"`
|
||||
Time string `json:"time"`
|
||||
Level string `json:"level"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type Logger struct {
|
||||
mu sync.Mutex
|
||||
nextID int64
|
||||
entries []LogEntry
|
||||
}
|
||||
|
||||
func NewLogger() *Logger { return &Logger{} }
|
||||
|
||||
func (l *Logger) Add(level, format string, args ...any) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
l.nextID++
|
||||
entry := LogEntry{ID: l.nextID, Time: time.Now().Format("15:04:05"), Level: level, Message: fmt.Sprintf(format, args...)}
|
||||
l.entries = append(l.entries, entry)
|
||||
if len(l.entries) > 600 {
|
||||
l.entries = l.entries[len(l.entries)-600:]
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Since(id int64) []LogEntry {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
out := make([]LogEntry, 0)
|
||||
for _, e := range l.entries {
|
||||
if e.ID > id {
|
||||
out = append(out, e)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
525
internal/engine/manager.go
Normal file
525
internal/engine/manager.go
Normal file
@@ -0,0 +1,525 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"socksrevivepc/internal/config"
|
||||
"socksrevivepc/internal/dnsttclient"
|
||||
"socksrevivepc/internal/routes"
|
||||
"socksrevivepc/internal/tun"
|
||||
)
|
||||
|
||||
type Status struct {
|
||||
Running bool `json:"running"`
|
||||
Connecting bool `json:"connecting"`
|
||||
ProfileID string `json:"profile_id"`
|
||||
Mode string `json:"mode"`
|
||||
SocksAddr string `json:"socks_addr"`
|
||||
Tun bool `json:"tun"`
|
||||
StartedAt string `json:"started_at"`
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
root string
|
||||
mu sync.Mutex
|
||||
logger *Logger
|
||||
status Status
|
||||
cancel context.CancelFunc
|
||||
ssh *sshBundle
|
||||
socks *SocksServer
|
||||
dnstt *ManagedProcess
|
||||
embeddedDNSTT *dnsttclient.Client
|
||||
xray *ManagedProcess
|
||||
tun *tun.Runner
|
||||
routeCleanup *routes.Cleanup
|
||||
manualStop bool
|
||||
reconnecting bool
|
||||
}
|
||||
|
||||
func NewManager(root string) *Manager {
|
||||
lg := NewLogger()
|
||||
return &Manager{root: root, logger: lg, tun: tun.NewRunner(lg)}
|
||||
}
|
||||
|
||||
func (m *Manager) LogsSince(id int64) []LogEntry { return m.logger.Since(id) }
|
||||
|
||||
func (m *Manager) Status() Status {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.status
|
||||
}
|
||||
|
||||
func (m *Manager) Start(p config.Profile) error {
|
||||
config.ApplyDefaults(&p)
|
||||
if err := config.Validate(p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
m.mu.Lock()
|
||||
if m.status.Running || m.status.Connecting {
|
||||
m.mu.Unlock()
|
||||
cancel()
|
||||
return fmt.Errorf("another profile is already running or connecting")
|
||||
}
|
||||
m.cancel = cancel
|
||||
m.manualStop = false
|
||||
m.status = Status{Connecting: true, ProfileID: p.ID, Mode: string(p.Mode), Tun: p.Tun.Enabled, StartedAt: time.Now().Format(time.RFC3339)}
|
||||
m.mu.Unlock()
|
||||
|
||||
m.logger.Add("info", "starting profile: %s", p.Name)
|
||||
|
||||
fail := func(format string, args ...any) error {
|
||||
err := fmt.Errorf(format, args...)
|
||||
m.logger.Add("error", "%v", err)
|
||||
m.stop(false)
|
||||
return err
|
||||
}
|
||||
|
||||
ensureCurrent := func() error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.cancel == nil || !m.status.Connecting || m.status.ProfileID != p.ID {
|
||||
return context.Canceled
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var socksAddr string
|
||||
var bypass []string
|
||||
if p.Mode == config.ModeXray {
|
||||
proc, err := StartProcess(ctx, m.root, "xray", p.Xray.Executable, p.Xray.Args, m.logger)
|
||||
if err != nil {
|
||||
return fail("start xray failed: %w", err)
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.xray = proc
|
||||
m.mu.Unlock()
|
||||
time.Sleep(time.Duration(p.Xray.StartupTimeoutMs) * time.Millisecond)
|
||||
if err := ensureCurrent(); err != nil {
|
||||
proc.Stop()
|
||||
return fail("connection cancelled: %w", err)
|
||||
}
|
||||
socksAddr = net.JoinHostPort(p.Xray.LocalSocksHost, fmt.Sprint(p.Xray.LocalSocksPort))
|
||||
if exited, procErr := proc.Exited(); exited {
|
||||
m.stop(false)
|
||||
if procErr != nil {
|
||||
return fmt.Errorf("xray exited before opening local SOCKS on %s: %w", socksAddr, procErr)
|
||||
}
|
||||
return fmt.Errorf("xray exited before opening local SOCKS on %s", socksAddr)
|
||||
}
|
||||
if err := waitForTCP(ctx, socksAddr, time.Duration(p.Xray.StartupTimeoutMs)*time.Millisecond); err != nil {
|
||||
return fail("xray local SOCKS is not listening on %s: %w", socksAddr, err)
|
||||
}
|
||||
bypass = nil
|
||||
} else {
|
||||
if p.Mode == config.ModeDNSTT {
|
||||
if p.DNSTT.UseEmbedded {
|
||||
client, err := dnsttclient.Start(ctx, dnsttclient.Options{
|
||||
ResolverType: p.DNSTT.ResolverType,
|
||||
ResolverAddress: p.DNSTT.ResolverAddress,
|
||||
PublicKeyHex: p.DNSTT.PublicKey,
|
||||
Domain: p.DNSTT.Domain,
|
||||
LocalAddress: net.JoinHostPort(p.DNSTT.LocalSSHHost, fmt.Sprint(p.DNSTT.LocalSSHPort)),
|
||||
UTLSDistribution: p.DNSTT.UTLSDistribution,
|
||||
StartupTimeout: time.Duration(p.DNSTT.StartupTimeoutMs) * time.Millisecond,
|
||||
LogWriter: dnsttLogWriter{logger: m.logger},
|
||||
})
|
||||
if err != nil {
|
||||
return fail("start embedded dnstt failed: %w", err)
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.embeddedDNSTT = client
|
||||
m.mu.Unlock()
|
||||
} else {
|
||||
proc, err := StartProcess(ctx, m.root, "dnstt", p.DNSTT.Executable, p.DNSTT.Args, m.logger)
|
||||
if err != nil {
|
||||
return fail("start dnstt failed: %w", err)
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.dnstt = proc
|
||||
m.mu.Unlock()
|
||||
time.Sleep(time.Duration(p.DNSTT.StartupTimeoutMs) * time.Millisecond)
|
||||
}
|
||||
if err := ensureCurrent(); err != nil {
|
||||
return fail("connection cancelled: %w", err)
|
||||
}
|
||||
}
|
||||
sshc, err := connectSSH(ctx, p, m.logger)
|
||||
if err != nil {
|
||||
return fail("%w", err)
|
||||
}
|
||||
if err := ensureCurrent(); err != nil {
|
||||
_ = sshc.Client.Close()
|
||||
_ = sshc.Conn.Close()
|
||||
return fail("connection cancelled: %w", err)
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.ssh = sshc
|
||||
m.mu.Unlock()
|
||||
socksAddr = net.JoinHostPort(p.Local.SocksHost, fmt.Sprint(p.Local.SocksPort))
|
||||
ss := &SocksServer{Addr: socksAddr, SSH: sshc.Client, Logger: m.logger, DNS: profileDNSServers(p), UDPGW: p.UDPGW}
|
||||
if err := ss.Start(); err != nil {
|
||||
return fail("%w", err)
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.socks = ss
|
||||
m.status.SocksAddr = socksAddr
|
||||
m.mu.Unlock()
|
||||
if err := waitForTCP(ctx, socksAddr, 2500*time.Millisecond); err != nil {
|
||||
return fail("local SOCKS is not listening on %s: %w", socksAddr, err)
|
||||
}
|
||||
bypass = effectiveBypassHosts(p, sshc.ControlHosts)
|
||||
}
|
||||
|
||||
if err := ensureCurrent(); err != nil {
|
||||
return fail("connection cancelled: %w", err)
|
||||
}
|
||||
if p.Tun.Enabled {
|
||||
if err := m.tun.Start(&p, socksAddr); err != nil {
|
||||
return fail("%w", err)
|
||||
}
|
||||
cleanup, err := routes.Apply(p, bypass, m.logger)
|
||||
if err != nil {
|
||||
m.logger.Add("warn", "route setup error: %v", err)
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.routeCleanup = cleanup
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
current := m.cancel != nil && m.status.Connecting && m.status.ProfileID == p.ID
|
||||
err := ctx.Err()
|
||||
if err == nil && current {
|
||||
m.status = Status{Running: true, ProfileID: p.ID, Mode: string(p.Mode), SocksAddr: socksAddr, Tun: p.Tun.Enabled, StartedAt: time.Now().Format(time.RFC3339)}
|
||||
}
|
||||
m.mu.Unlock()
|
||||
if err != nil || !current {
|
||||
m.stop(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return context.Canceled
|
||||
}
|
||||
m.logger.Add("info", "profile is connected; local socks=%s tun=%v", socksAddr, p.Tun.Enabled)
|
||||
m.startMonitor(ctx, p)
|
||||
return nil
|
||||
}
|
||||
|
||||
func profileDNSServers(p config.Profile) []string {
|
||||
out := append([]string{}, p.Tun.DNS...)
|
||||
if p.Tun.IPv6Enabled {
|
||||
out = append(out, p.Tun.IPv6DNS...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func waitForTCP(ctx context.Context, addr string, timeout time.Duration) error {
|
||||
if timeout <= 0 {
|
||||
timeout = 5 * time.Second
|
||||
}
|
||||
deadline := time.Now().Add(timeout)
|
||||
var lastErr error
|
||||
for time.Now().Before(deadline) {
|
||||
d := net.Dialer{Timeout: 350 * time.Millisecond}
|
||||
c, err := d.DialContext(ctx, "tcp", addr)
|
||||
if err == nil {
|
||||
_ = c.Close()
|
||||
return nil
|
||||
}
|
||||
lastErr = err
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(150 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
if lastErr != nil {
|
||||
return lastErr
|
||||
}
|
||||
return fmt.Errorf("timeout waiting for %s", addr)
|
||||
}
|
||||
|
||||
func effectiveBypassHosts(p config.Profile, activeControlHosts []string) []string {
|
||||
seen := map[string]bool{}
|
||||
out := make([]string, 0, len(activeControlHosts)+1)
|
||||
add := func(host string) {
|
||||
host = strings.TrimSpace(host)
|
||||
if host == "" {
|
||||
return
|
||||
}
|
||||
if h, _, err := net.SplitHostPort(host); err == nil {
|
||||
host = strings.Trim(h, "[]")
|
||||
}
|
||||
if isLocalBypassHost(host) || seen[host] {
|
||||
return
|
||||
}
|
||||
seen[host] = true
|
||||
out = append(out, host)
|
||||
}
|
||||
for _, host := range activeControlHosts {
|
||||
add(host)
|
||||
}
|
||||
if len(out) == 0 && p.Mode != config.ModeDNSTT {
|
||||
// Fallback for old profile formats or unexpected transports. This still only
|
||||
// adds the direct SSH host, not every rotated proxy from the profile.
|
||||
add(p.SSH.Host)
|
||||
}
|
||||
if p.Mode == config.ModeDNSTT && p.DNSTT.UseEmbedded {
|
||||
add(dnsttResolverHost(p.DNSTT.ResolverAddress))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func isLocalBypassHost(host string) bool {
|
||||
host = strings.Trim(strings.ToLower(strings.TrimSpace(host)), "[]")
|
||||
if host == "" || host == "localhost" {
|
||||
return true
|
||||
}
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return ip.IsLoopback() || ip.IsUnspecified()
|
||||
}
|
||||
|
||||
func dnsttResolverHost(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.Contains(s, "://") {
|
||||
u, err := url.Parse(s)
|
||||
if err == nil {
|
||||
return u.Hostname()
|
||||
}
|
||||
}
|
||||
host, _, err := net.SplitHostPort(s)
|
||||
if err == nil {
|
||||
return host
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (m *Manager) Stop() {
|
||||
m.stop(true)
|
||||
}
|
||||
|
||||
func (m *Manager) stop(manual bool) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if manual {
|
||||
m.manualStop = true
|
||||
}
|
||||
m.stopLocked()
|
||||
}
|
||||
|
||||
func (m *Manager) startMonitor(ctx context.Context, p config.Profile) {
|
||||
if !p.Reconnect.Enabled {
|
||||
return
|
||||
}
|
||||
interval := time.Duration(p.Reconnect.CheckIntervalSeconds) * time.Second
|
||||
if interval <= 0 {
|
||||
interval = 10 * time.Second
|
||||
}
|
||||
go func() {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if m.isManualStopped() {
|
||||
return
|
||||
}
|
||||
if err := m.probeConnection(p); err != nil {
|
||||
m.logger.Add("warn", "connection monitor detected tunnel loss: %v", err)
|
||||
m.reconnectLoop(p, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (m *Manager) isManualStopped() bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.manualStop
|
||||
}
|
||||
|
||||
func (m *Manager) markReconnecting() bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.manualStop || m.reconnecting {
|
||||
return false
|
||||
}
|
||||
m.reconnecting = true
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Manager) clearReconnecting() {
|
||||
m.mu.Lock()
|
||||
m.reconnecting = false
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func (m *Manager) probeConnection(p config.Profile) error {
|
||||
m.mu.Lock()
|
||||
sshc := m.ssh
|
||||
xray := m.xray
|
||||
socksAddr := m.status.SocksAddr
|
||||
running := m.status.Running
|
||||
m.mu.Unlock()
|
||||
|
||||
if !running {
|
||||
return nil
|
||||
}
|
||||
if xray != nil {
|
||||
if exited, err := xray.Exited(); exited {
|
||||
if err != nil {
|
||||
return fmt.Errorf("xray exited: %w", err)
|
||||
}
|
||||
return fmt.Errorf("xray exited")
|
||||
}
|
||||
if socksAddr != "" {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
return waitForTCP(ctx, socksAddr, 2*time.Second)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if sshc != nil && sshc.Client != nil {
|
||||
if sshc.Conn != nil {
|
||||
_ = sshc.Conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
defer sshc.Conn.SetDeadline(time.Time{})
|
||||
}
|
||||
_, _, err := sshc.Client.SendRequest("keepalive@openssh.com", true, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ssh keepalive failed: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) reconnectLoop(p config.Profile, cause error) {
|
||||
if !m.markReconnecting() {
|
||||
return
|
||||
}
|
||||
defer m.clearReconnecting()
|
||||
|
||||
delay := time.Duration(p.Reconnect.DelaySeconds) * time.Second
|
||||
if delay <= 0 {
|
||||
delay = 3 * time.Second
|
||||
}
|
||||
maxRetries := p.Reconnect.MaxRetries
|
||||
m.logger.Add("warn", "connection lost (%v); auto reconnect is enabled", cause)
|
||||
if p.Tun.Enabled {
|
||||
m.logger.Add("info", "destroying TUN before reconnect")
|
||||
}
|
||||
m.stop(false)
|
||||
if p.Tun.Enabled {
|
||||
time.Sleep(1200 * time.Millisecond)
|
||||
}
|
||||
|
||||
for attempt := 1; maxRetries <= 0 || attempt <= maxRetries; attempt++ {
|
||||
if m.isManualStopped() {
|
||||
m.logger.Add("info", "auto reconnect cancelled by user")
|
||||
return
|
||||
}
|
||||
m.logger.Add("info", "reconnect attempt %d%s in %s", attempt, reconnectLimitSuffix(maxRetries), delay)
|
||||
select {
|
||||
case <-time.After(delay):
|
||||
}
|
||||
if m.isManualStopped() {
|
||||
m.logger.Add("info", "auto reconnect cancelled by user")
|
||||
return
|
||||
}
|
||||
if err := m.Start(p); err != nil {
|
||||
if m.isManualStopped() {
|
||||
m.logger.Add("info", "auto reconnect cancelled by user")
|
||||
return
|
||||
}
|
||||
m.logger.Add("warn", "reconnect attempt %d failed: %v", attempt, err)
|
||||
if p.Tun.Enabled {
|
||||
m.logger.Add("info", "destroying TUN after failed reconnect attempt")
|
||||
m.stop(false)
|
||||
time.Sleep(1200 * time.Millisecond)
|
||||
}
|
||||
continue
|
||||
}
|
||||
m.logger.Add("info", "reconnected successfully")
|
||||
return
|
||||
}
|
||||
m.logger.Add("error", "auto reconnect stopped after %d failed attempt(s)", maxRetries)
|
||||
}
|
||||
|
||||
func reconnectLimitSuffix(maxRetries int) string {
|
||||
if maxRetries <= 0 {
|
||||
return " (unlimited)"
|
||||
}
|
||||
return fmt.Sprintf("/%d", maxRetries)
|
||||
}
|
||||
|
||||
func (m *Manager) stopLocked() {
|
||||
wasActive := m.status.Running || m.status.Connecting
|
||||
if m.cancel != nil {
|
||||
m.cancel()
|
||||
m.cancel = nil
|
||||
}
|
||||
if m.routeCleanup != nil {
|
||||
m.routeCleanup.Run()
|
||||
m.routeCleanup = nil
|
||||
}
|
||||
if m.tun != nil {
|
||||
m.tun.Stop()
|
||||
}
|
||||
if m.socks != nil {
|
||||
m.socks.Stop()
|
||||
m.socks = nil
|
||||
}
|
||||
if m.ssh != nil {
|
||||
_ = m.ssh.Client.Close()
|
||||
_ = m.ssh.Conn.Close()
|
||||
m.ssh = nil
|
||||
}
|
||||
if m.xray != nil {
|
||||
m.xray.Stop()
|
||||
m.xray = nil
|
||||
}
|
||||
if m.embeddedDNSTT != nil {
|
||||
m.embeddedDNSTT.Stop()
|
||||
m.embeddedDNSTT = nil
|
||||
}
|
||||
if m.dnstt != nil {
|
||||
m.dnstt.Stop()
|
||||
m.dnstt = nil
|
||||
}
|
||||
if wasActive {
|
||||
m.logger.Add("info", "disconnected")
|
||||
}
|
||||
m.status = Status{}
|
||||
}
|
||||
|
||||
type dnsttLogWriter struct {
|
||||
logger *Logger
|
||||
}
|
||||
|
||||
func (w dnsttLogWriter) Write(p []byte) (int, error) {
|
||||
line := strings.TrimSpace(string(p))
|
||||
if line != "" && w.logger != nil {
|
||||
w.logger.Add("dnstt", "%s", line)
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
399
internal/engine/payload.go
Normal file
399
internal/engine/payload.go
Normal file
@@ -0,0 +1,399 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"socksrevivepc/internal/config"
|
||||
)
|
||||
|
||||
const (
|
||||
payloadHTTPStatusPeekTimeoutMs = 1500
|
||||
payloadHTTPStatusLineLimit = 4096
|
||||
maxPayloadHTTPResponsesToSkip = 12
|
||||
maxPayloadHTTPHeaderLines = 80
|
||||
maxPayloadHTTPBodyDiscardBytes = 1024 * 1024
|
||||
)
|
||||
|
||||
type PayloadResult struct {
|
||||
StatusLine string
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
type httpResponseInfo struct {
|
||||
contentLength int64
|
||||
chunked bool
|
||||
}
|
||||
|
||||
type preloadedConn struct {
|
||||
net.Conn
|
||||
preloaded *bytes.Reader
|
||||
}
|
||||
|
||||
func (c *preloadedConn) Read(p []byte) (int, error) {
|
||||
if c.preloaded != nil && c.preloaded.Len() > 0 {
|
||||
return c.preloaded.Read(p)
|
||||
}
|
||||
return c.Conn.Read(p)
|
||||
}
|
||||
|
||||
func wrapConnWithPreloadedBytes(conn net.Conn, b []byte) net.Conn {
|
||||
if len(b) == 0 {
|
||||
return conn
|
||||
}
|
||||
return &preloadedConn{Conn: conn, preloaded: bytes.NewReader(b)}
|
||||
}
|
||||
|
||||
func WritePayload(conn net.Conn, p config.Profile, targetHost string, targetPort int, logger *Logger) (PayloadResult, net.Conn, error) {
|
||||
payload := buildPayload(p.Payload.Text, targetHost, targetPort)
|
||||
parts, instant := splitPayload(payload)
|
||||
for i, part := range parts {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := io.WriteString(conn, part); err != nil {
|
||||
return PayloadResult{}, conn, err
|
||||
}
|
||||
if i < len(parts)-1 && !instant {
|
||||
time.Sleep(time.Duration(p.Payload.SplitDelayMs) * time.Millisecond)
|
||||
}
|
||||
}
|
||||
logger.Add("debug", "payload sent (%d bytes)", len(payload))
|
||||
|
||||
if !p.Payload.WaitForResponse {
|
||||
return PayloadResult{}, conn, nil
|
||||
}
|
||||
|
||||
return consumePayloadHTTPNegotiation(conn, p, payloadSourceLabel(p.Mode), logger)
|
||||
}
|
||||
|
||||
func consumePayloadHTTPNegotiation(conn net.Conn, p config.Profile, source string, logger *Logger) (PayloadResult, net.Conn, error) {
|
||||
defer conn.SetReadDeadline(time.Time{})
|
||||
|
||||
var last PayloadResult
|
||||
var captured *bytes.Buffer
|
||||
var sawSuccess bool
|
||||
|
||||
for attempt := 0; attempt < maxPayloadHTTPResponsesToSkip; attempt++ {
|
||||
setPayloadReadDeadline(conn, p, attempt)
|
||||
captured = &bytes.Buffer{}
|
||||
line, err := readPayloadLinePreserveBytes(conn, captured, payloadHTTPStatusLineLimit)
|
||||
if err != nil {
|
||||
if isTimeoutErr(err) {
|
||||
if last.StatusCode >= 400 && !p.Payload.AcceptAnyStatus && !sawSuccess {
|
||||
return last, conn, fmt.Errorf("payload rejected with final status %d", last.StatusCode)
|
||||
}
|
||||
return last, wrapConnWithPreloadedBytes(conn, captured.Bytes()), nil
|
||||
}
|
||||
if err == io.EOF && captured.Len() > 0 {
|
||||
return last, wrapConnWithPreloadedBytes(conn, captured.Bytes()), nil
|
||||
}
|
||||
if last.StatusCode > 0 {
|
||||
return last, conn, nil
|
||||
}
|
||||
return PayloadResult{}, conn, fmt.Errorf("payload response read failed: %w", err)
|
||||
}
|
||||
|
||||
cleanLine := strings.TrimSpace(line)
|
||||
if cleanLine == "" {
|
||||
return last, wrapConnWithPreloadedBytes(conn, captured.Bytes()), nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(cleanLine, "SSH-") || !isHTTPStatusLine(cleanLine) {
|
||||
return last, wrapConnWithPreloadedBytes(conn, captured.Bytes()), nil
|
||||
}
|
||||
|
||||
code := parseStatusCode(cleanLine)
|
||||
last = PayloadResult{StatusLine: cleanLine, StatusCode: code}
|
||||
logProxyStatus(logger, source, code, cleanLine)
|
||||
logHTTPCompatibilityStatus(logger, code, cleanLine)
|
||||
if code == 101 || (code >= 200 && code < 400) {
|
||||
sawSuccess = true
|
||||
}
|
||||
|
||||
// The current bytes are confirmed HTTP/proxy negotiation bytes. Do not replay
|
||||
// them to the SSH transport. Only replay bytes when we detect SSH/non-HTTP
|
||||
// data or a partial line after timeout.
|
||||
captured = nil
|
||||
if err := consumePayloadHTTPHeadersAndBody(conn); err != nil {
|
||||
if isTimeoutErr(err) {
|
||||
return last, conn, nil
|
||||
}
|
||||
return last, conn, fmt.Errorf("payload response consume failed: %w", err)
|
||||
}
|
||||
|
||||
// Keep peeking for another immediate HTTP status block. Some payload/proxy
|
||||
// chains return several statuses (for example 403 -> 403 -> 101). Returning
|
||||
// after the first status can make SSH read HTTP text instead of SSH-2.0.
|
||||
}
|
||||
|
||||
if last.StatusCode >= 400 && !p.Payload.AcceptAnyStatus && !sawSuccess {
|
||||
return last, conn, fmt.Errorf("payload rejected with final status %d", last.StatusCode)
|
||||
}
|
||||
return last, conn, nil
|
||||
}
|
||||
|
||||
func setPayloadReadDeadline(conn net.Conn, p config.Profile, attempt int) {
|
||||
timeoutMs := payloadHTTPStatusPeekTimeoutMs
|
||||
if attempt == 0 && p.Payload.ResponseTimeoutMs > 0 {
|
||||
timeoutMs = p.Payload.ResponseTimeoutMs
|
||||
if timeoutMs < payloadHTTPStatusPeekTimeoutMs {
|
||||
timeoutMs = payloadHTTPStatusPeekTimeoutMs
|
||||
}
|
||||
}
|
||||
_ = conn.SetReadDeadline(time.Now().Add(time.Duration(timeoutMs) * time.Millisecond))
|
||||
}
|
||||
|
||||
func readPayloadLinePreserveBytes(conn net.Conn, captured *bytes.Buffer, limit int) (string, error) {
|
||||
var line bytes.Buffer
|
||||
buf := make([]byte, 1)
|
||||
for line.Len() < limit {
|
||||
n, err := conn.Read(buf)
|
||||
if n > 0 {
|
||||
b := buf[0]
|
||||
_ = captured.WriteByte(b)
|
||||
_ = line.WriteByte(b)
|
||||
if b == '\n' {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if line.Len() > 0 && err == io.EOF {
|
||||
return line.String(), nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
if n == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if line.Len() == 0 {
|
||||
return "", io.EOF
|
||||
}
|
||||
return line.String(), nil
|
||||
}
|
||||
|
||||
func consumePayloadHTTPHeadersAndBody(conn net.Conn) error {
|
||||
info := httpResponseInfo{contentLength: -1}
|
||||
for i := 0; i < maxPayloadHTTPHeaderLines; i++ {
|
||||
ignored := &bytes.Buffer{}
|
||||
line, err := readPayloadLinePreserveBytes(conn, ignored, payloadHTTPStatusLineLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clean := strings.TrimSpace(line)
|
||||
if clean == "" {
|
||||
break
|
||||
}
|
||||
lower := strings.ToLower(clean)
|
||||
if strings.HasPrefix(lower, "content-length:") {
|
||||
if n, err := strconv.ParseInt(strings.TrimSpace(clean[strings.Index(clean, ":")+1:]), 10, 64); err == nil {
|
||||
info.contentLength = n
|
||||
}
|
||||
} else if strings.HasPrefix(lower, "transfer-encoding:") && strings.Contains(lower, "chunked") {
|
||||
info.chunked = true
|
||||
}
|
||||
}
|
||||
if info.chunked {
|
||||
return discardPayloadChunkedBody(conn)
|
||||
}
|
||||
if info.contentLength > 0 {
|
||||
return discardPayloadFixedLengthBody(conn, info.contentLength)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func discardPayloadFixedLengthBody(conn net.Conn, contentLength int64) error {
|
||||
remaining := contentLength
|
||||
if remaining > maxPayloadHTTPBodyDiscardBytes {
|
||||
remaining = maxPayloadHTTPBodyDiscardBytes
|
||||
}
|
||||
buf := make([]byte, 4096)
|
||||
for remaining > 0 {
|
||||
toRead := int64(len(buf))
|
||||
if remaining < toRead {
|
||||
toRead = remaining
|
||||
}
|
||||
n, err := conn.Read(buf[:int(toRead)])
|
||||
if n > 0 {
|
||||
remaining -= int64(n)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func discardPayloadChunkedBody(conn net.Conn) error {
|
||||
for i := 0; i < maxPayloadHTTPHeaderLines; i++ {
|
||||
ignored := &bytes.Buffer{}
|
||||
sizeLine, err := readPayloadLinePreserveBytes(conn, ignored, payloadHTTPStatusLineLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cleanSize := strings.TrimSpace(sizeLine)
|
||||
if semi := strings.Index(cleanSize, ";"); semi >= 0 {
|
||||
cleanSize = strings.TrimSpace(cleanSize[:semi])
|
||||
}
|
||||
chunkSize, err := strconv.ParseInt(cleanSize, 16, 64)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if chunkSize == 0 {
|
||||
return consumePayloadTrailingHeaders(conn)
|
||||
}
|
||||
if err := discardPayloadFixedLengthBody(conn, chunkSize); err != nil {
|
||||
return err
|
||||
}
|
||||
crlf := &bytes.Buffer{}
|
||||
_, _ = readPayloadLinePreserveBytes(conn, crlf, payloadHTTPStatusLineLimit)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func consumePayloadTrailingHeaders(conn net.Conn) error {
|
||||
for i := 0; i < maxPayloadHTTPHeaderLines; i++ {
|
||||
ignored := &bytes.Buffer{}
|
||||
line, err := readPayloadLinePreserveBytes(conn, ignored, payloadHTTPStatusLineLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(line) == "" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isTimeoutErr(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isHTTPStatusLine(statusLine string) bool {
|
||||
clean := strings.ToUpper(strings.TrimSpace(statusLine))
|
||||
return strings.HasPrefix(clean, "HTTP/1.") || strings.HasPrefix(clean, "HTTP/2") || strings.HasPrefix(clean, "HTTP/3")
|
||||
}
|
||||
|
||||
func logProxyStatus(logger *Logger, source string, responseCode int, statusLine string) {
|
||||
cleanLine := strings.TrimSpace(statusLine)
|
||||
if cleanLine == "" {
|
||||
return
|
||||
}
|
||||
if source == "" {
|
||||
source = "PROXY"
|
||||
}
|
||||
logger.Add("info", "Proxy Status [%s]: %s", source, cleanLine)
|
||||
}
|
||||
|
||||
func logHTTPCompatibilityStatus(logger *Logger, responseCode int, firstLine string) {
|
||||
switch responseCode {
|
||||
case 200:
|
||||
logger.Add("info", "Status: 200 (Connection established) Successful")
|
||||
case 101:
|
||||
logger.Add("info", "replace 200 OK")
|
||||
logger.Add("info", "HTTP/1.1 101 Websocket")
|
||||
case 100:
|
||||
logger.Add("info", "HTTP/1.1 100 Continue")
|
||||
case 301, 302, 400, 401, 403, 404, 407, 429, 500, 502, 503, 504:
|
||||
if strings.TrimSpace(firstLine) != "" {
|
||||
logger.Add("info", "%s", strings.TrimSpace(firstLine))
|
||||
} else {
|
||||
logger.Add("info", "HTTP/1.1 %d", responseCode)
|
||||
}
|
||||
logger.Add("info", "replace 200 OK")
|
||||
logger.Add("info", "Dragon Try!")
|
||||
}
|
||||
}
|
||||
|
||||
func payloadSourceLabel(mode config.Mode) string {
|
||||
switch mode {
|
||||
case config.ModePayload:
|
||||
return "HTTP_PROXY"
|
||||
case config.ModePayloadSSL:
|
||||
return "SSL_PAYLOAD"
|
||||
default:
|
||||
return "PROXY"
|
||||
}
|
||||
}
|
||||
|
||||
func buildPayload(tpl, host string, port int) string {
|
||||
portStr := strconv.Itoa(port)
|
||||
repl := map[string]string{
|
||||
"[host]": host,
|
||||
"[port]": portStr,
|
||||
"[host_port]": net.JoinHostPort(host, portStr),
|
||||
"[crlf]": "\r\n",
|
||||
"[cr]": "\r",
|
||||
"[lf]": "\n",
|
||||
"[protocol]": "HTTP/1.1",
|
||||
"[method]": "CONNECT",
|
||||
}
|
||||
out := tpl
|
||||
for k, v := range repl {
|
||||
out = strings.ReplaceAll(out, k, v)
|
||||
}
|
||||
out = replaceRotate(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func replaceRotate(s string) string {
|
||||
for {
|
||||
start := strings.Index(s, "[rotate=")
|
||||
if start < 0 {
|
||||
return s
|
||||
}
|
||||
end := strings.Index(s[start:], "]")
|
||||
if end < 0 {
|
||||
return s
|
||||
}
|
||||
end += start
|
||||
body := strings.TrimPrefix(s[start:end+1], "[rotate=")
|
||||
body = strings.TrimSuffix(body, "]")
|
||||
choices := splitRotateChoices(body)
|
||||
choice := ""
|
||||
if len(choices) > 0 {
|
||||
choice = strings.TrimSpace(choices[rand.Intn(len(choices))])
|
||||
}
|
||||
s = s[:start] + choice + s[end+1:]
|
||||
}
|
||||
}
|
||||
|
||||
func splitRotateChoices(body string) []string {
|
||||
return strings.FieldsFunc(body, func(r rune) bool {
|
||||
switch r {
|
||||
case ';', '#', ',', '\n', '\r', '\t':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func splitPayload(s string) ([]string, bool) {
|
||||
instant := strings.Contains(s, "[instant_split]")
|
||||
s = strings.ReplaceAll(s, "[instant_split]", "[split]")
|
||||
parts := strings.Split(s, "[split]")
|
||||
return parts, instant
|
||||
}
|
||||
|
||||
func parseStatusCode(status string) int {
|
||||
fields := strings.Fields(status)
|
||||
if len(fields) < 2 {
|
||||
return 0
|
||||
}
|
||||
code, _ := strconv.Atoi(fields[1])
|
||||
return code
|
||||
}
|
||||
91
internal/engine/process.go
Normal file
91
internal/engine/process.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"socksrevivepc/internal/oscmd"
|
||||
)
|
||||
|
||||
type ManagedProcess struct {
|
||||
cmd *exec.Cmd
|
||||
name string
|
||||
logger *Logger
|
||||
mu sync.Mutex
|
||||
done chan error
|
||||
}
|
||||
|
||||
func StartProcess(ctx context.Context, root, name, exe string, args []string, logger *Logger) (*ManagedProcess, error) {
|
||||
if strings.TrimSpace(exe) == "" {
|
||||
return nil, errors.New(name + " executable path is empty")
|
||||
}
|
||||
if !filepath.IsAbs(exe) {
|
||||
exe = filepath.Join(root, exe)
|
||||
}
|
||||
cmd := oscmd.CommandContext(ctx, exe, args...)
|
||||
cmd.Dir = root
|
||||
stdout, _ := cmd.StdoutPipe()
|
||||
stderr, _ := cmd.StderrPipe()
|
||||
p := &ManagedProcess{cmd: cmd, name: name, logger: logger, done: make(chan error, 1)}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.Add("info", "%s started: %s %s", name, exe, strings.Join(args, " "))
|
||||
go p.pipe(stdout, "info")
|
||||
go p.pipe(stderr, "warn")
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
p.done <- err
|
||||
if err != nil {
|
||||
logger.Add("warn", "%s stopped: %v", name, err)
|
||||
} else {
|
||||
logger.Add("info", "%s stopped", name)
|
||||
}
|
||||
}()
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *ManagedProcess) Exited() (bool, error) {
|
||||
if p == nil || p.done == nil {
|
||||
return true, nil
|
||||
}
|
||||
select {
|
||||
case err := <-p.done:
|
||||
return true, err
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ManagedProcess) pipe(r io.Reader, level string) {
|
||||
s := bufio.NewScanner(r)
|
||||
for s.Scan() {
|
||||
line := strings.TrimSpace(s.Text())
|
||||
if line != "" {
|
||||
p.logger.Add(level, "%s: %s", p.name, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ManagedProcess) Stop() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p == nil || p.cmd == nil || p.cmd.Process == nil {
|
||||
return
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
_ = p.cmd.Process.Kill()
|
||||
} else {
|
||||
_ = p.cmd.Process.Signal(ioSignalInterrupt())
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
_ = p.cmd.Process.Kill()
|
||||
}
|
||||
}
|
||||
7
internal/engine/signal_unix.go
Normal file
7
internal/engine/signal_unix.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build !windows
|
||||
|
||||
package engine
|
||||
|
||||
import "os"
|
||||
|
||||
func ioSignalInterrupt() os.Signal { return os.Interrupt }
|
||||
7
internal/engine/signal_windows.go
Normal file
7
internal/engine/signal_windows.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build windows
|
||||
|
||||
package engine
|
||||
|
||||
import "os"
|
||||
|
||||
func ioSignalInterrupt() os.Signal { return os.Interrupt }
|
||||
462
internal/engine/socks5.go
Normal file
462
internal/engine/socks5.go
Normal file
@@ -0,0 +1,462 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"socksrevivepc/internal/config"
|
||||
)
|
||||
|
||||
const (
|
||||
socksVersion = 0x05
|
||||
socksCmdConnect = 0x01
|
||||
socksCmdUDPAssoc = 0x03
|
||||
socksAtypIPv4 = 0x01
|
||||
socksAtypDomain = 0x03
|
||||
socksAtypIPv6 = 0x04
|
||||
socksRepOK = 0x00
|
||||
socksRepFail = 0x01
|
||||
socksRepUnsupported = 0x07
|
||||
)
|
||||
|
||||
type SocksServer struct {
|
||||
Addr string
|
||||
SSH *ssh.Client
|
||||
Logger *Logger
|
||||
DNS []string
|
||||
UDPGW config.UDPGWConfig
|
||||
listener net.Listener
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
type socksRequest struct {
|
||||
Command byte
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
func (r socksRequest) Addr() string {
|
||||
return net.JoinHostPort(r.Host, strconv.Itoa(r.Port))
|
||||
}
|
||||
|
||||
func (s *SocksServer) Start() error {
|
||||
ln, err := net.Listen("tcp", s.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.listener = ln
|
||||
s.Logger.Add("info", "local SOCKS5 listening on %s", s.Addr)
|
||||
go s.acceptLoop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SocksServer) Stop() {
|
||||
s.stopOnce.Do(func() {
|
||||
if s.listener != nil {
|
||||
_ = s.listener.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SocksServer) acceptLoop() {
|
||||
for {
|
||||
c, err := s.listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go s.handle(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SocksServer) handle(c net.Conn) {
|
||||
defer c.Close()
|
||||
_ = c.SetDeadline(time.Now().Add(30 * time.Second))
|
||||
if err := s.handshake(c); err != nil {
|
||||
if !isExpectedProbeError(err) {
|
||||
s.Logger.Add("debug", "socks handshake failed: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
req, err := readSocksRequest(c)
|
||||
if err != nil {
|
||||
s.Logger.Add("debug", "socks request failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
switch req.Command {
|
||||
case socksCmdConnect:
|
||||
s.handleConnect(c, req)
|
||||
case socksCmdUDPAssoc:
|
||||
if s.UDPGW.Enabled {
|
||||
s.handleUDPGWAssociate(c)
|
||||
} else {
|
||||
s.handleDNSUDPAssociate(c)
|
||||
}
|
||||
default:
|
||||
_ = writeReply(c, socksRepUnsupported)
|
||||
s.Logger.Add("debug", "socks unsupported command %d", req.Command)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SocksServer) handleConnect(c net.Conn, req socksRequest) {
|
||||
dest := req.Addr()
|
||||
remote, err := dialSSHDirectTCP(s.SSH, req, c.RemoteAddr(), s.Logger)
|
||||
if err != nil {
|
||||
_ = writeReply(c, 0x05)
|
||||
s.Logger.Add("warn", "ssh dial %s failed: %v", dest, err)
|
||||
return
|
||||
}
|
||||
defer remote.Close()
|
||||
_ = writeReply(c, socksRepOK)
|
||||
_ = c.SetDeadline(time.Time{})
|
||||
s.Logger.Add("debug", "socks connected %s", dest)
|
||||
proxyCopy(c, remote)
|
||||
}
|
||||
|
||||
func (s *SocksServer) handleDNSUDPAssociate(c net.Conn) {
|
||||
udp, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||
if err != nil {
|
||||
_ = writeReply(c, socksRepFail)
|
||||
s.Logger.Add("warn", "socks UDP associate failed: %v", err)
|
||||
return
|
||||
}
|
||||
defer udp.Close()
|
||||
|
||||
if err := writeReplyWithAddr(c, socksRepOK, udp.LocalAddr().(*net.UDPAddr)); err != nil {
|
||||
s.Logger.Add("debug", "socks UDP associate reply failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
_ = c.SetDeadline(time.Time{})
|
||||
s.Logger.Add("info", "local SOCKS5 UDP associate listening on %s for DNS over SSH", udp.LocalAddr().String())
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
_, _ = io.Copy(io.Discard, c)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
buf := make([]byte, 64*1024)
|
||||
for {
|
||||
_ = udp.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
|
||||
n, clientAddr, err := udp.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
||||
continue
|
||||
}
|
||||
s.Logger.Add("debug", "socks UDP read failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := s.handleSocksUDPDatagram(buf[:n])
|
||||
if err != nil {
|
||||
if shouldLogUDPError(err) {
|
||||
s.Logger.Add("debug", "%v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
_, _ = udp.WriteToUDP(resp, clientAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SocksServer) handleSocksUDPDatagram(packet []byte) ([]byte, error) {
|
||||
addr, payload, err := parseSocksUDP(packet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if addr.Port != 53 {
|
||||
return nil, fmt.Errorf("dropping unsupported UDP target %s; only DNS/UDP port 53 is proxied for SSH modes", addr.Addr())
|
||||
}
|
||||
answer, err := s.resolveDNSOverSSHTCP(addr, payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("DNS over SSH failed for %s: %w", addr.Addr(), err)
|
||||
}
|
||||
return buildSocksUDP(addr, answer)
|
||||
}
|
||||
|
||||
func (s *SocksServer) resolveDNSOverSSHTCP(addr socksRequest, query []byte) ([]byte, error) {
|
||||
servers := s.dnsServers(addr)
|
||||
var lastErr error
|
||||
for _, server := range servers {
|
||||
remote, err := dialSSHDirectTCPAddr(s.SSH, server, nil, s.Logger)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
_ = remote.SetDeadline(time.Now().Add(8 * time.Second))
|
||||
resp, err := exchangeDNSTCP(remote, query)
|
||||
_ = remote.Close()
|
||||
if err == nil {
|
||||
return resp, nil
|
||||
}
|
||||
lastErr = err
|
||||
}
|
||||
if lastErr != nil {
|
||||
return nil, lastErr
|
||||
}
|
||||
return nil, errors.New("no DNS server configured")
|
||||
}
|
||||
|
||||
func (s *SocksServer) dnsServers(requested socksRequest) []string {
|
||||
seen := map[string]bool{}
|
||||
var out []string
|
||||
add := func(v string) {
|
||||
v = normalizeHostPort(v, "53")
|
||||
if v == "" {
|
||||
return
|
||||
}
|
||||
if !seen[v] {
|
||||
seen[v] = true
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
if requested.Host != "" {
|
||||
add(net.JoinHostPort(requested.Host, strconv.Itoa(requested.Port)))
|
||||
}
|
||||
for _, dns := range s.DNS {
|
||||
add(dns)
|
||||
}
|
||||
add("1.1.1.1:53")
|
||||
add("8.8.8.8:53")
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeHostPort(v, defaultPort string) string {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
return ""
|
||||
}
|
||||
if host, port, err := net.SplitHostPort(v); err == nil {
|
||||
return net.JoinHostPort(host, port)
|
||||
}
|
||||
if ip := net.ParseIP(v); ip != nil {
|
||||
return net.JoinHostPort(ip.String(), defaultPort)
|
||||
}
|
||||
if strings.Count(v, ":") == 1 {
|
||||
host, port, ok := strings.Cut(v, ":")
|
||||
if ok && host != "" && port != "" {
|
||||
return net.JoinHostPort(host, port)
|
||||
}
|
||||
}
|
||||
return net.JoinHostPort(v, defaultPort)
|
||||
}
|
||||
|
||||
func exchangeDNSTCP(conn net.Conn, query []byte) ([]byte, error) {
|
||||
if len(query) == 0 || len(query) > 65535 {
|
||||
return nil, fmt.Errorf("invalid DNS query length %d", len(query))
|
||||
}
|
||||
prefix := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(prefix, uint16(len(query)))
|
||||
if _, err := conn.Write(append(prefix, query...)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := io.ReadFull(conn, prefix); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ln := int(binary.BigEndian.Uint16(prefix))
|
||||
if ln <= 0 || ln > 65535 {
|
||||
return nil, fmt.Errorf("invalid DNS response length %d", ln)
|
||||
}
|
||||
resp := make([]byte, ln)
|
||||
if _, err := io.ReadFull(conn, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *SocksServer) handshake(c net.Conn) error {
|
||||
header := make([]byte, 2)
|
||||
if _, err := io.ReadFull(c, header); err != nil {
|
||||
return err
|
||||
}
|
||||
if header[0] != socksVersion {
|
||||
return errors.New("not socks5")
|
||||
}
|
||||
methods := make([]byte, int(header[1]))
|
||||
if _, err := io.ReadFull(c, methods); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := c.Write([]byte{socksVersion, 0x00})
|
||||
return err
|
||||
}
|
||||
|
||||
func readSocksRequest(c net.Conn) (socksRequest, error) {
|
||||
var req socksRequest
|
||||
h := make([]byte, 4)
|
||||
if _, err := io.ReadFull(c, h); err != nil {
|
||||
return req, err
|
||||
}
|
||||
if h[0] != socksVersion {
|
||||
return req, fmt.Errorf("invalid socks version %d", h[0])
|
||||
}
|
||||
req.Command = h[1]
|
||||
host, err := readSocksHost(c, h[3])
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
pb := make([]byte, 2)
|
||||
if _, err := io.ReadFull(c, pb); err != nil {
|
||||
return req, err
|
||||
}
|
||||
req.Host = host
|
||||
req.Port = int(binary.BigEndian.Uint16(pb))
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func readSocksHost(r io.Reader, atyp byte) (string, error) {
|
||||
switch atyp {
|
||||
case socksAtypIPv4:
|
||||
b := make([]byte, 4)
|
||||
if _, err := io.ReadFull(r, b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return net.IP(b).String(), nil
|
||||
case socksAtypDomain:
|
||||
l := []byte{0}
|
||||
if _, err := io.ReadFull(r, l); err != nil {
|
||||
return "", err
|
||||
}
|
||||
b := make([]byte, int(l[0]))
|
||||
if _, err := io.ReadFull(r, b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), nil
|
||||
case socksAtypIPv6:
|
||||
b := make([]byte, 16)
|
||||
if _, err := io.ReadFull(r, b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return net.IP(b).String(), nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported address type %d", atyp)
|
||||
}
|
||||
}
|
||||
|
||||
func parseSocksUDP(packet []byte) (socksRequest, []byte, error) {
|
||||
var req socksRequest
|
||||
if len(packet) < 4 {
|
||||
return req, nil, errors.New("short socks UDP packet")
|
||||
}
|
||||
if packet[0] != 0 || packet[1] != 0 {
|
||||
return req, nil, errors.New("invalid socks UDP reserved field")
|
||||
}
|
||||
if packet[2] != 0 {
|
||||
return req, nil, errors.New("fragmented socks UDP packets are not supported")
|
||||
}
|
||||
atyp := packet[3]
|
||||
off := 4
|
||||
switch atyp {
|
||||
case socksAtypIPv4:
|
||||
if len(packet) < off+4+2 {
|
||||
return req, nil, errors.New("short socks UDP ipv4 packet")
|
||||
}
|
||||
req.Host = net.IP(packet[off : off+4]).String()
|
||||
off += 4
|
||||
case socksAtypDomain:
|
||||
if len(packet) < off+1 {
|
||||
return req, nil, errors.New("short socks UDP domain packet")
|
||||
}
|
||||
ln := int(packet[off])
|
||||
off++
|
||||
if len(packet) < off+ln+2 {
|
||||
return req, nil, errors.New("short socks UDP domain payload")
|
||||
}
|
||||
req.Host = string(packet[off : off+ln])
|
||||
off += ln
|
||||
case socksAtypIPv6:
|
||||
if len(packet) < off+16+2 {
|
||||
return req, nil, errors.New("short socks UDP ipv6 packet")
|
||||
}
|
||||
req.Host = net.IP(packet[off : off+16]).String()
|
||||
off += 16
|
||||
default:
|
||||
return req, nil, fmt.Errorf("unsupported socks UDP address type %d", atyp)
|
||||
}
|
||||
req.Port = int(binary.BigEndian.Uint16(packet[off : off+2]))
|
||||
off += 2
|
||||
return req, packet[off:], nil
|
||||
}
|
||||
|
||||
func buildSocksUDP(addr socksRequest, payload []byte) ([]byte, error) {
|
||||
var out []byte
|
||||
out = append(out, 0, 0, 0)
|
||||
ip := net.ParseIP(addr.Host)
|
||||
if v4 := ip.To4(); v4 != nil {
|
||||
out = append(out, socksAtypIPv4)
|
||||
out = append(out, v4...)
|
||||
} else if v6 := ip.To16(); v6 != nil {
|
||||
out = append(out, socksAtypIPv6)
|
||||
out = append(out, v6...)
|
||||
} else {
|
||||
if len(addr.Host) > 255 {
|
||||
return nil, errors.New("socks UDP domain is too long")
|
||||
}
|
||||
out = append(out, socksAtypDomain, byte(len(addr.Host)))
|
||||
out = append(out, []byte(addr.Host)...)
|
||||
}
|
||||
pb := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(pb, uint16(addr.Port))
|
||||
out = append(out, pb...)
|
||||
out = append(out, payload...)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func writeReply(c net.Conn, code byte) error {
|
||||
return writeReplyWithAddr(c, code, &net.UDPAddr{IP: net.IPv4zero, Port: 0})
|
||||
}
|
||||
|
||||
func writeReplyWithAddr(c net.Conn, code byte, addr *net.UDPAddr) error {
|
||||
if addr == nil {
|
||||
addr = &net.UDPAddr{IP: net.IPv4zero, Port: 0}
|
||||
}
|
||||
ip := addr.IP
|
||||
if ip == nil || ip.IsUnspecified() {
|
||||
ip = net.IPv4(127, 0, 0, 1)
|
||||
}
|
||||
var resp []byte
|
||||
resp = append(resp, socksVersion, code, 0x00)
|
||||
if v4 := ip.To4(); v4 != nil {
|
||||
resp = append(resp, socksAtypIPv4)
|
||||
resp = append(resp, v4...)
|
||||
} else if v6 := ip.To16(); v6 != nil {
|
||||
resp = append(resp, socksAtypIPv6)
|
||||
resp = append(resp, v6...)
|
||||
} else {
|
||||
resp = append(resp, socksAtypIPv4, 127, 0, 0, 1)
|
||||
}
|
||||
pb := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(pb, uint16(addr.Port))
|
||||
resp = append(resp, pb...)
|
||||
_, err := c.Write(resp)
|
||||
return err
|
||||
}
|
||||
|
||||
func proxyCopy(a net.Conn, b net.Conn) {
|
||||
done := make(chan struct{}, 2)
|
||||
go func() { _, _ = io.Copy(a, b); done <- struct{}{} }()
|
||||
go func() { _, _ = io.Copy(b, a); done <- struct{}{} }()
|
||||
<-done
|
||||
}
|
||||
|
||||
func isExpectedProbeError(err error) bool {
|
||||
return errors.Is(err, io.EOF) || strings.Contains(err.Error(), "connection reset")
|
||||
}
|
||||
|
||||
func shouldLogUDPError(err error) bool {
|
||||
msg := err.Error()
|
||||
return !strings.Contains(msg, "unsupported UDP target")
|
||||
}
|
||||
151
internal/engine/ssh_direct_tcp.go
Normal file
151
internal/engine/ssh_direct_tcp.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type directTCPIPPayload struct {
|
||||
DestAddr string
|
||||
DestPort uint32
|
||||
OriginAddr string
|
||||
OriginPort uint32
|
||||
}
|
||||
|
||||
type sshChannelConn struct {
|
||||
ssh.Channel
|
||||
local net.Addr
|
||||
remote net.Addr
|
||||
}
|
||||
|
||||
func (c *sshChannelConn) LocalAddr() net.Addr {
|
||||
return c.local
|
||||
}
|
||||
|
||||
func (c *sshChannelConn) RemoteAddr() net.Addr {
|
||||
return c.remote
|
||||
}
|
||||
|
||||
func (c *sshChannelConn) SetDeadline(time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *sshChannelConn) SetReadDeadline(time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *sshChannelConn) SetWriteDeadline(time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func dialSSHDirectTCP(client *ssh.Client, dest socksRequest, origin net.Addr, logger *Logger) (net.Conn, error) {
|
||||
addr := dest.Addr()
|
||||
remote, err := client.Dial("tcp", addr)
|
||||
if err == nil {
|
||||
return remote, nil
|
||||
}
|
||||
|
||||
if !shouldRetrySSHIPv6Bracket(dest.Host, err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if logger != nil {
|
||||
logger.Add("debug", "retrying SSH direct-tcpip IPv6 target with bracketed host [%s]:%d", strings.Trim(dest.Host, "[]"), dest.Port)
|
||||
}
|
||||
remote, retryErr := openSSHDirectTCPIP(client, bracketIPv6Host(dest.Host), dest.Port, origin)
|
||||
if retryErr == nil {
|
||||
return remote, nil
|
||||
}
|
||||
return nil, fmt.Errorf("%w; bracketed IPv6 retry failed: %v", err, retryErr)
|
||||
}
|
||||
|
||||
func dialSSHDirectTCPAddr(client *ssh.Client, addr string, origin net.Addr, logger *Logger) (net.Conn, error) {
|
||||
host, portText, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return client.Dial("tcp", addr)
|
||||
}
|
||||
port, err := strconv.Atoi(portText)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dialSSHDirectTCP(client, socksRequest{Host: host, Port: port}, origin, logger)
|
||||
}
|
||||
|
||||
func openSSHDirectTCPIP(client *ssh.Client, destHost string, destPort int, origin net.Addr) (net.Conn, error) {
|
||||
originHost, originPort := splitOriginAddr(origin)
|
||||
payload := ssh.Marshal(directTCPIPPayload{
|
||||
DestAddr: destHost,
|
||||
DestPort: uint32(destPort),
|
||||
OriginAddr: originHost,
|
||||
OriginPort: uint32(originPort),
|
||||
})
|
||||
ch, reqs, err := client.OpenChannel("direct-tcpip", payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go ssh.DiscardRequests(reqs)
|
||||
return &sshChannelConn{
|
||||
Channel: ch,
|
||||
local: tcpAddrOrDummy(originHost, originPort),
|
||||
remote: tcpAddrOrDummy(destHost, destPort),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func shouldRetrySSHIPv6Bracket(host string, err error) bool {
|
||||
if err == nil || !isIPv6Literal(host) {
|
||||
return false
|
||||
}
|
||||
msg := strings.ToLower(err.Error())
|
||||
return strings.Contains(msg, "too many colons in address") || strings.Contains(msg, "missing port in address")
|
||||
}
|
||||
|
||||
func isIPv6Literal(host string) bool {
|
||||
host = strings.Trim(host, "[]")
|
||||
ip := net.ParseIP(host)
|
||||
return ip != nil && ip.To4() == nil && ip.To16() != nil
|
||||
}
|
||||
|
||||
func bracketIPv6Host(host string) string {
|
||||
host = strings.TrimSpace(host)
|
||||
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
|
||||
return host
|
||||
}
|
||||
return "[" + strings.Trim(host, "[]") + "]"
|
||||
}
|
||||
|
||||
func splitOriginAddr(addr net.Addr) (string, int) {
|
||||
if addr == nil {
|
||||
return "127.0.0.1", 0
|
||||
}
|
||||
host, portText, err := net.SplitHostPort(addr.String())
|
||||
if err != nil {
|
||||
return "127.0.0.1", 0
|
||||
}
|
||||
port, err := strconv.Atoi(portText)
|
||||
if err != nil {
|
||||
port = 0
|
||||
}
|
||||
if host == "" || host == "::" || host == "0.0.0.0" {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
return host, port
|
||||
}
|
||||
|
||||
func tcpAddrOrDummy(host string, port int) net.Addr {
|
||||
h := strings.Trim(host, "[]")
|
||||
ip := net.ParseIP(h)
|
||||
if ip != nil {
|
||||
return &net.TCPAddr{IP: ip, Port: port}
|
||||
}
|
||||
return dummyAddr(net.JoinHostPort(h, strconv.Itoa(port)))
|
||||
}
|
||||
|
||||
type dummyAddr string
|
||||
|
||||
func (d dummyAddr) Network() string { return "tcp" }
|
||||
func (d dummyAddr) String() string { return string(d) }
|
||||
345
internal/engine/sshclient.go
Normal file
345
internal/engine/sshclient.go
Normal file
@@ -0,0 +1,345 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
"socksrevivepc/internal/config"
|
||||
)
|
||||
|
||||
type sshBundle struct {
|
||||
Client *ssh.Client
|
||||
Conn net.Conn
|
||||
ControlHosts []string
|
||||
}
|
||||
|
||||
type transportAttempt struct {
|
||||
Label string
|
||||
ProxyHost string
|
||||
ProxyPort int
|
||||
TLSHost string
|
||||
TLSPort int
|
||||
ControlHost string
|
||||
}
|
||||
|
||||
func connectSSH(ctx context.Context, p config.Profile, logger *Logger) (*sshBundle, error) {
|
||||
targetHost := p.SSH.Host
|
||||
targetPort := p.SSH.Port
|
||||
if p.Mode == config.ModeDNSTT {
|
||||
targetHost = p.DNSTT.LocalSSHHost
|
||||
targetPort = p.DNSTT.LocalSSHPort
|
||||
}
|
||||
|
||||
attempts := buildTransportAttempts(p, targetHost, targetPort)
|
||||
if len(attempts) == 0 {
|
||||
attempts = []transportAttempt{{Label: "default"}}
|
||||
}
|
||||
|
||||
logger.Add("info", "connecting SSH %s:%d using mode %s", targetHost, targetPort, p.Mode)
|
||||
var lastErr error
|
||||
for i, attempt := range attempts {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
if len(attempts) > 1 {
|
||||
logger.Add("info", "connection attempt %d/%d via %s", i+1, len(attempts), attempt.Label)
|
||||
}
|
||||
|
||||
conn, err := dialTransportAttempt(ctx, p, targetHost, targetPort, attempt, logger)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
if len(attempts) > 1 {
|
||||
logger.Add("warn", "connection attempt %d/%d failed before SSH handshake: %v", i+1, len(attempts), err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
bundle, err := finishSSHHandshake(conn, p, targetHost, targetPort, logger)
|
||||
if err == nil {
|
||||
bundle.ControlHosts = controlHostsForAttempt(p, targetHost, attempt)
|
||||
if len(attempts) > 1 {
|
||||
logger.Add("info", "connection attempt %d/%d succeeded via %s", i+1, len(attempts), attempt.Label)
|
||||
}
|
||||
return bundle, nil
|
||||
}
|
||||
_ = conn.Close()
|
||||
lastErr = err
|
||||
if len(attempts) > 1 {
|
||||
logger.Add("warn", "connection attempt %d/%d failed during SSH handshake: %v", i+1, len(attempts), err)
|
||||
}
|
||||
}
|
||||
|
||||
if lastErr == nil {
|
||||
lastErr = fmt.Errorf("no transport attempts were available")
|
||||
}
|
||||
if len(attempts) > 1 {
|
||||
return nil, fmt.Errorf("all %d connection attempts failed; last error: %w", len(attempts), lastErr)
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
func finishSSHHandshake(conn net.Conn, p config.Profile, targetHost string, targetPort int, logger *Logger) (*sshBundle, error) {
|
||||
sshCfg := &ssh.ClientConfig{
|
||||
User: p.SSH.Username,
|
||||
Auth: []ssh.AuthMethod{ssh.Password(p.SSH.Password)},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
Timeout: time.Duration(p.SSH.HandshakeTimeoutMs) * time.Millisecond,
|
||||
ClientVersion: "SSH-2.0-SocksRevivePC",
|
||||
}
|
||||
addr := net.JoinHostPort(targetHost, fmt.Sprint(targetPort))
|
||||
cc, chans, reqs, err := ssh.NewClientConn(conn, addr, sshCfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ssh handshake failed: %w", err)
|
||||
}
|
||||
client := ssh.NewClient(cc, chans, reqs)
|
||||
logger.Add("info", "ssh authenticated as %s", p.SSH.Username)
|
||||
return &sshBundle{Client: client, Conn: conn}, nil
|
||||
}
|
||||
|
||||
func buildTransportAttempts(p config.Profile, targetHost string, targetPort int) []transportAttempt {
|
||||
switch p.Mode {
|
||||
case config.ModePayload:
|
||||
return payloadProxyAttempts(p, targetHost, targetPort)
|
||||
case config.ModeSSL, config.ModePayloadSSL:
|
||||
return tlsHostAttempts(p, targetHost, targetPort)
|
||||
default:
|
||||
return []transportAttempt{{Label: net.JoinHostPort(targetHost, fmt.Sprint(targetPort))}}
|
||||
}
|
||||
}
|
||||
|
||||
func controlHostsForAttempt(p config.Profile, targetHost string, attempt transportAttempt) []string {
|
||||
switch p.Mode {
|
||||
case config.ModePayload:
|
||||
if attempt.ProxyHost != "" {
|
||||
return []string{attempt.ProxyHost}
|
||||
}
|
||||
return []string{targetHost}
|
||||
case config.ModeSSL, config.ModePayloadSSL:
|
||||
if attempt.TLSHost != "" {
|
||||
return []string{attempt.TLSHost}
|
||||
}
|
||||
if p.TLS.Host != "" {
|
||||
host, _ := hostPortWithDefault(p.TLS.Host, p.TLS.Port)
|
||||
if host != "" {
|
||||
return []string{host}
|
||||
}
|
||||
}
|
||||
return []string{targetHost}
|
||||
default:
|
||||
return []string{targetHost}
|
||||
}
|
||||
}
|
||||
|
||||
func payloadProxyAttempts(p config.Profile, targetHost string, targetPort int) []transportAttempt {
|
||||
proxyText := strings.TrimSpace(p.Proxy.Host)
|
||||
if proxyText == "" || p.Proxy.Port <= 0 {
|
||||
return []transportAttempt{{Label: "direct payload transport"}}
|
||||
}
|
||||
|
||||
rawHosts := splitHostList(proxyText)
|
||||
if len(rawHosts) == 0 {
|
||||
return []transportAttempt{{Label: "direct payload transport"}}
|
||||
}
|
||||
out := make([]transportAttempt, 0, len(rawHosts))
|
||||
for _, raw := range rawHosts {
|
||||
host, port := hostPortWithDefault(raw, p.Proxy.Port)
|
||||
if host == "" || port <= 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, transportAttempt{ProxyHost: host, ProxyPort: port, Label: "proxy " + net.JoinHostPort(host, fmt.Sprint(port))})
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return []transportAttempt{{Label: net.JoinHostPort(targetHost, fmt.Sprint(targetPort))}}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func tlsHostAttempts(p config.Profile, targetHost string, targetPort int) []transportAttempt {
|
||||
hostText := strings.TrimSpace(p.TLS.Host)
|
||||
defaultPort := p.TLS.Port
|
||||
if defaultPort <= 0 {
|
||||
defaultPort = targetPort
|
||||
}
|
||||
if hostText == "" {
|
||||
return []transportAttempt{{TLSHost: targetHost, TLSPort: defaultPort, Label: "TLS " + net.JoinHostPort(targetHost, fmt.Sprint(defaultPort))}}
|
||||
}
|
||||
|
||||
rawHosts := splitHostList(hostText)
|
||||
if len(rawHosts) == 0 {
|
||||
return []transportAttempt{{TLSHost: targetHost, TLSPort: defaultPort, Label: "TLS " + net.JoinHostPort(targetHost, fmt.Sprint(defaultPort))}}
|
||||
}
|
||||
out := make([]transportAttempt, 0, len(rawHosts))
|
||||
for _, raw := range rawHosts {
|
||||
host, port := hostPortWithDefault(raw, defaultPort)
|
||||
if host == "" || port <= 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, transportAttempt{TLSHost: host, TLSPort: port, Label: "TLS " + net.JoinHostPort(host, fmt.Sprint(port))})
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return []transportAttempt{{TLSHost: targetHost, TLSPort: defaultPort, Label: "TLS " + net.JoinHostPort(targetHost, fmt.Sprint(defaultPort))}}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func dialTransport(ctx context.Context, p config.Profile, targetHost string, targetPort int, logger *Logger) (net.Conn, error) {
|
||||
attempts := buildTransportAttempts(p, targetHost, targetPort)
|
||||
if len(attempts) == 0 {
|
||||
attempts = []transportAttempt{{Label: "default"}}
|
||||
}
|
||||
return dialTransportAttempt(ctx, p, targetHost, targetPort, attempts[0], logger)
|
||||
}
|
||||
|
||||
func dialTransportAttempt(ctx context.Context, p config.Profile, targetHost string, targetPort int, attempt transportAttempt, logger *Logger) (net.Conn, error) {
|
||||
d := &net.Dialer{Timeout: time.Duration(p.SSH.HandshakeTimeoutMs) * time.Millisecond}
|
||||
addr := net.JoinHostPort(targetHost, fmt.Sprint(targetPort))
|
||||
|
||||
switch p.Mode {
|
||||
case config.ModeDirect, config.ModeDNSTT:
|
||||
return d.DialContext(ctx, "tcp", addr)
|
||||
case config.ModeSSL:
|
||||
return dialTLSAttempt(ctx, d, p, targetHost, targetPort, attempt)
|
||||
case config.ModePayload:
|
||||
connectHost, connectPort := targetHost, targetPort
|
||||
if attempt.ProxyHost != "" && attempt.ProxyPort > 0 {
|
||||
connectHost, connectPort = attempt.ProxyHost, attempt.ProxyPort
|
||||
} else if p.Proxy.Host != "" && p.Proxy.Port > 0 {
|
||||
connectHost, connectPort = p.Proxy.Host, p.Proxy.Port
|
||||
}
|
||||
logger.Add("debug", "payload transport dialing %s", net.JoinHostPort(connectHost, fmt.Sprint(connectPort)))
|
||||
conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(connectHost, fmt.Sprint(connectPort)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, wrappedConn, err := WritePayload(conn, p, targetHost, targetPort, logger); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
} else {
|
||||
conn = wrappedConn
|
||||
}
|
||||
return conn, nil
|
||||
case config.ModePayloadSSL:
|
||||
conn, err := dialTLSAttempt(ctx, d, p, targetHost, targetPort, attempt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, wrappedConn, err := WritePayload(conn, p, targetHost, targetPort, logger); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
} else {
|
||||
conn = wrappedConn
|
||||
}
|
||||
return conn, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported mode %s", p.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func dialTLS(ctx context.Context, d *net.Dialer, p config.Profile, targetHost string, targetPort int) (net.Conn, error) {
|
||||
return dialTLSAttempt(ctx, d, p, targetHost, targetPort, transportAttempt{})
|
||||
}
|
||||
|
||||
func dialTLSAttempt(ctx context.Context, d *net.Dialer, p config.Profile, targetHost string, targetPort int, attempt transportAttempt) (net.Conn, error) {
|
||||
host := strings.TrimSpace(attempt.TLSHost)
|
||||
port := attempt.TLSPort
|
||||
if host == "" {
|
||||
host = p.TLS.Host
|
||||
}
|
||||
if port <= 0 {
|
||||
port = p.TLS.Port
|
||||
}
|
||||
if host == "" {
|
||||
host = targetHost
|
||||
}
|
||||
if port == 0 {
|
||||
port = targetPort
|
||||
}
|
||||
serverName := p.TLS.ServerName
|
||||
if serverName == "" {
|
||||
serverName = host
|
||||
}
|
||||
raw, err := d.DialContext(ctx, "tcp", net.JoinHostPort(host, fmt.Sprint(port)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg := &tls.Config{ServerName: serverName, InsecureSkipVerify: p.TLS.InsecureSkipVerify, MinVersion: tls.VersionTLS12}
|
||||
conn := tls.Client(raw, cfg)
|
||||
if err := conn.HandshakeContext(ctx); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func splitHostList(s string) []string {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.FieldsFunc(s, func(r rune) bool {
|
||||
switch r {
|
||||
case '#', ',', ';', '\n', '\r', '\t':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
out := make([]string, 0, len(parts))
|
||||
seen := map[string]struct{}{}
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[part]; ok {
|
||||
continue
|
||||
}
|
||||
seen[part] = struct{}{}
|
||||
out = append(out, part)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func hostPortWithDefault(raw string, defaultPort int) (string, int) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return "", defaultPort
|
||||
}
|
||||
if strings.Contains(raw, "://") {
|
||||
// These fields are host fields, not URLs. Strip the scheme if a user pastes one.
|
||||
if i := strings.Index(raw, "://"); i >= 0 {
|
||||
raw = raw[i+3:]
|
||||
}
|
||||
}
|
||||
if h, p, err := net.SplitHostPort(raw); err == nil {
|
||||
pi, _ := strconv.Atoi(p)
|
||||
if pi > 0 {
|
||||
return strings.Trim(h, "[]"), pi
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(raw, "[") && strings.Contains(raw, "]:") {
|
||||
if h, p, err := net.SplitHostPort(raw); err == nil {
|
||||
pi, _ := strconv.Atoi(p)
|
||||
if pi > 0 {
|
||||
return strings.Trim(h, "[]"), pi
|
||||
}
|
||||
}
|
||||
}
|
||||
// host:port for IPv4/domain. IPv6 without brackets has multiple colons and
|
||||
// must keep the profile/default port.
|
||||
if strings.Count(raw, ":") == 1 {
|
||||
host, portText, ok := strings.Cut(raw, ":")
|
||||
if ok {
|
||||
if pi, err := strconv.Atoi(strings.TrimSpace(portText)); err == nil && pi > 0 {
|
||||
return strings.TrimSpace(host), pi
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Trim(raw, "[]"), defaultPort
|
||||
}
|
||||
353
internal/engine/udpgw_client.go
Normal file
353
internal/engine/udpgw_client.go
Normal file
@@ -0,0 +1,353 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
udpgwMaxFrame = 64 * 1024
|
||||
|
||||
udpgwProtocolBadVPN = "badvpn"
|
||||
udpgwProtocolLegacy = "legacy"
|
||||
|
||||
udpgwFlagKeepAlive = 1 << 0
|
||||
udpgwFlagRebind = 1 << 1
|
||||
udpgwFlagDNS = 1 << 2
|
||||
udpgwFlagIPv6 = 1 << 3
|
||||
)
|
||||
|
||||
type udpgwSession struct {
|
||||
mu sync.Mutex
|
||||
nextID uint16
|
||||
pairID map[string]uint16
|
||||
idClient map[uint16]*net.UDPAddr
|
||||
}
|
||||
|
||||
func (s *SocksServer) handleUDPGWAssociate(c net.Conn) {
|
||||
udp, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||
if err != nil {
|
||||
_ = writeReply(c, socksRepFail)
|
||||
s.Logger.Add("warn", "socks UDP associate failed: %v", err)
|
||||
return
|
||||
}
|
||||
defer udp.Close()
|
||||
|
||||
gwAddr := net.JoinHostPort(s.UDPGW.Host, strconv.Itoa(s.UDPGW.Port))
|
||||
gwConn, err := dialSSHDirectTCPAddr(s.SSH, gwAddr, c.RemoteAddr(), s.Logger)
|
||||
if err != nil {
|
||||
_ = writeReply(c, socksRepFail)
|
||||
s.Logger.Add("warn", "udpgw dial through SSH failed %s: %v", gwAddr, err)
|
||||
return
|
||||
}
|
||||
defer gwConn.Close()
|
||||
|
||||
if err := writeReplyWithAddr(c, socksRepOK, udp.LocalAddr().(*net.UDPAddr)); err != nil {
|
||||
s.Logger.Add("debug", "socks UDP associate reply failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
_ = c.SetDeadline(time.Time{})
|
||||
proto := normalizeUDPGWProtocol(s.UDPGW.Protocol)
|
||||
s.Logger.Add("info", "SOCKS5 UDP associate using UDPGW %s over SSH; protocol=%s local UDP=%s", gwAddr, proto, udp.LocalAddr().String())
|
||||
|
||||
done := make(chan struct{})
|
||||
closeOnce := sync.Once{}
|
||||
stop := func() {
|
||||
closeOnce.Do(func() {
|
||||
_ = c.Close()
|
||||
_ = gwConn.Close()
|
||||
_ = udp.Close()
|
||||
close(done)
|
||||
})
|
||||
}
|
||||
|
||||
go func() {
|
||||
_, _ = io.Copy(io.Discard, c)
|
||||
stop()
|
||||
}()
|
||||
|
||||
sess := &udpgwSession{
|
||||
nextID: 1,
|
||||
pairID: make(map[string]uint16),
|
||||
idClient: make(map[uint16]*net.UDPAddr),
|
||||
}
|
||||
|
||||
go s.readUDPGWReplies(done, proto, gwConn, udp, sess)
|
||||
s.forwardUDPToUDPGW(done, proto, gwConn, udp, sess)
|
||||
stop()
|
||||
}
|
||||
|
||||
func normalizeUDPGWProtocol(v string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(v)) {
|
||||
case udpgwProtocolLegacy:
|
||||
return udpgwProtocolLegacy
|
||||
default:
|
||||
return udpgwProtocolBadVPN
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SocksServer) forwardUDPToUDPGW(done <-chan struct{}, proto string, gwConn net.Conn, udp *net.UDPConn, sess *udpgwSession) {
|
||||
buf := make([]byte, 64*1024)
|
||||
for {
|
||||
_ = udp.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
|
||||
n, clientAddr, err := udp.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
||||
continue
|
||||
}
|
||||
s.Logger.Add("debug", "udpgw local UDP read failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
addr, payload, err := parseSocksUDP(buf[:n])
|
||||
if err != nil {
|
||||
if shouldLogUDPError(err) {
|
||||
s.Logger.Add("debug", "udpgw parse SOCKS UDP failed: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
connID, isNew, err := sess.idFor(clientAddr, addr)
|
||||
if err != nil {
|
||||
s.Logger.Add("debug", "udpgw session id failed for %s: %v", addr.Addr(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
frame, err := udpgwBuildClientFrame(proto, connID, isNew, addr, payload)
|
||||
if err != nil {
|
||||
if shouldLogUDPError(err) {
|
||||
s.Logger.Add("debug", "udpgw frame build failed for %s: %v", addr.Addr(), err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
_ = gwConn.SetWriteDeadline(time.Now().Add(15 * time.Second))
|
||||
if _, err := gwConn.Write(frame); err != nil {
|
||||
s.Logger.Add("warn", "udpgw TCP write failed: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SocksServer) readUDPGWReplies(done <-chan struct{}, proto string, gwConn net.Conn, udp *net.UDPConn, sess *udpgwSession) {
|
||||
br := bufio.NewReaderSize(gwConn, 256*1024)
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
payload, err := udpgwReadFrame(br, udpgwMaxFrame)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
if !errors.Is(err, io.EOF) {
|
||||
s.Logger.Add("debug", "udpgw TCP read failed: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
connID, src, data, err := udpgwParseReplyPayload(proto, payload)
|
||||
if err != nil {
|
||||
s.Logger.Add("debug", "udpgw bad reply frame: %v", err)
|
||||
continue
|
||||
}
|
||||
clientAddr := sess.clientFor(connID)
|
||||
if clientAddr == nil {
|
||||
continue
|
||||
}
|
||||
packet, err := buildSocksUDP(src, data)
|
||||
if err != nil {
|
||||
s.Logger.Add("debug", "udpgw SOCKS UDP reply build failed: %v", err)
|
||||
continue
|
||||
}
|
||||
_, _ = udp.WriteToUDP(packet, clientAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *udpgwSession) idFor(client *net.UDPAddr, dest socksRequest) (uint16, bool, error) {
|
||||
if client == nil {
|
||||
return 0, false, errors.New("missing local UDP client address")
|
||||
}
|
||||
key := client.String() + "|" + dest.Addr()
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if id, ok := s.pairID[key]; ok {
|
||||
s.idClient[id] = cloneUDPAddr(client)
|
||||
return id, false, nil
|
||||
}
|
||||
id := s.nextID
|
||||
if id == 0 {
|
||||
id = 1
|
||||
}
|
||||
s.nextID = id + 1
|
||||
if s.nextID == 0 {
|
||||
s.nextID = 1
|
||||
}
|
||||
s.pairID[key] = id
|
||||
s.idClient[id] = cloneUDPAddr(client)
|
||||
return id, true, nil
|
||||
}
|
||||
|
||||
func (s *udpgwSession) clientFor(id uint16) *net.UDPAddr {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return cloneUDPAddr(s.idClient[id])
|
||||
}
|
||||
|
||||
func cloneUDPAddr(a *net.UDPAddr) *net.UDPAddr {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
out := *a
|
||||
if a.IP != nil {
|
||||
out.IP = append(net.IP(nil), a.IP...)
|
||||
}
|
||||
return &out
|
||||
}
|
||||
|
||||
func udpgwReadFrame(r *bufio.Reader, max int) ([]byte, error) {
|
||||
var lenBuf [2]byte
|
||||
if _, err := io.ReadFull(r, lenBuf[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n := int(binary.LittleEndian.Uint16(lenBuf[:]))
|
||||
if n <= 0 || n > max {
|
||||
return nil, fmt.Errorf("invalid udpgw frame length %d", n)
|
||||
}
|
||||
b := make([]byte, n)
|
||||
if _, err := io.ReadFull(r, b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func udpgwBuildClientFrame(proto string, connID uint16, isNew bool, dest socksRequest, data []byte) ([]byte, error) {
|
||||
if normalizeUDPGWProtocol(proto) == udpgwProtocolLegacy {
|
||||
return udpgwBuildLegacyFrame(connID, 0, dest, data)
|
||||
}
|
||||
return udpgwBuildBadVPNFrame(connID, isNew, dest, data)
|
||||
}
|
||||
|
||||
func udpgwParseReplyPayload(proto string, payload []byte) (uint16, socksRequest, []byte, error) {
|
||||
if normalizeUDPGWProtocol(proto) == udpgwProtocolLegacy {
|
||||
return udpgwParseLegacyReplyPayload(payload)
|
||||
}
|
||||
return udpgwParseBadVPNReplyPayload(payload)
|
||||
}
|
||||
|
||||
// udpgwBuildBadVPNFrame implements the normal badvpn-udpgw PacketProto frame:
|
||||
// length(little-endian uint16) + flags(1) + conid(little-endian uint16) +
|
||||
// IPv4/IPv6 destination + destination port(network byte order) + UDP payload.
|
||||
// This is the same framing used by the Android badvpn UDPGW client and supports
|
||||
// IPv6 targets with UDPGW_CLIENT_FLAG_IPV6.
|
||||
func udpgwBuildBadVPNFrame(connID uint16, isNew bool, dest socksRequest, data []byte) ([]byte, error) {
|
||||
ip := net.ParseIP(dest.Host)
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("badvpn UDPGW requires an IP target, got %q", dest.Host)
|
||||
}
|
||||
flags := byte(0)
|
||||
if isNew {
|
||||
flags |= udpgwFlagRebind
|
||||
}
|
||||
|
||||
var addr []byte
|
||||
if v4 := ip.To4(); v4 != nil {
|
||||
addr = append(addr, v4...)
|
||||
} else if v6 := ip.To16(); v6 != nil {
|
||||
flags |= udpgwFlagIPv6
|
||||
addr = append(addr, v6...)
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid UDPGW target IP %q", dest.Host)
|
||||
}
|
||||
|
||||
payloadLen := 1 + 2 + len(addr) + 2 + len(data)
|
||||
if payloadLen <= 0 || payloadLen > 65535 {
|
||||
return nil, fmt.Errorf("UDPGW payload too large: %d", payloadLen)
|
||||
}
|
||||
out := make([]byte, 2+payloadLen)
|
||||
binary.LittleEndian.PutUint16(out[0:2], uint16(payloadLen))
|
||||
out[2] = flags
|
||||
binary.LittleEndian.PutUint16(out[3:5], connID)
|
||||
copy(out[5:5+len(addr)], addr)
|
||||
portOff := 5 + len(addr)
|
||||
binary.BigEndian.PutUint16(out[portOff:portOff+2], uint16(dest.Port))
|
||||
copy(out[portOff+2:], data)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func udpgwParseBadVPNReplyPayload(payload []byte) (uint16, socksRequest, []byte, error) {
|
||||
var src socksRequest
|
||||
if len(payload) < 1+2+4+2 {
|
||||
return 0, src, nil, fmt.Errorf("short badvpn udpgw payload %d", len(payload))
|
||||
}
|
||||
flags := payload[0]
|
||||
if flags&udpgwFlagKeepAlive != 0 {
|
||||
return 0, src, nil, errors.New("unexpected udpgw keepalive reply")
|
||||
}
|
||||
connID := binary.LittleEndian.Uint16(payload[1:3])
|
||||
off := 3
|
||||
if flags&udpgwFlagIPv6 != 0 {
|
||||
if len(payload) < off+16+2 {
|
||||
return 0, src, nil, fmt.Errorf("short badvpn udpgw ipv6 payload %d", len(payload))
|
||||
}
|
||||
src.Host = net.IP(payload[off : off+16]).String()
|
||||
off += 16
|
||||
} else {
|
||||
if len(payload) < off+4+2 {
|
||||
return 0, src, nil, fmt.Errorf("short badvpn udpgw ipv4 payload %d", len(payload))
|
||||
}
|
||||
src.Host = net.IP(payload[off : off+4]).String()
|
||||
off += 4
|
||||
}
|
||||
src.Port = int(binary.BigEndian.Uint16(payload[off : off+2]))
|
||||
off += 2
|
||||
return connID, src, payload[off:], nil
|
||||
}
|
||||
|
||||
// Legacy frame kept only for older experimental PC builds. It is IPv4-only.
|
||||
func udpgwBuildLegacyFrame(connID uint16, x byte, dest socksRequest, data []byte) ([]byte, error) {
|
||||
ip := net.ParseIP(dest.Host)
|
||||
v4 := ip.To4()
|
||||
if v4 == nil {
|
||||
return nil, fmt.Errorf("legacy UDPGW only supports IPv4 UDP targets; got %s", dest.Addr())
|
||||
}
|
||||
payloadLen := 2 + 1 + 4 + 2 + len(data)
|
||||
if payloadLen <= 0 || payloadLen > 65535 {
|
||||
return nil, fmt.Errorf("UDPGW payload too large: %d", payloadLen)
|
||||
}
|
||||
out := make([]byte, 2+payloadLen)
|
||||
binary.LittleEndian.PutUint16(out[0:2], uint16(payloadLen))
|
||||
binary.BigEndian.PutUint16(out[2:4], connID)
|
||||
out[4] = x
|
||||
copy(out[5:9], v4)
|
||||
binary.BigEndian.PutUint16(out[9:11], uint16(dest.Port))
|
||||
copy(out[11:], data)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func udpgwParseLegacyReplyPayload(payload []byte) (uint16, socksRequest, []byte, error) {
|
||||
var src socksRequest
|
||||
if len(payload) < 2+1+4+2 {
|
||||
return 0, src, nil, fmt.Errorf("short legacy udpgw payload %d", len(payload))
|
||||
}
|
||||
connID := binary.BigEndian.Uint16(payload[0:2])
|
||||
src.Host = net.IP(payload[3:7]).String()
|
||||
src.Port = int(binary.BigEndian.Uint16(payload[7:9]))
|
||||
return connID, src, payload[9:], nil
|
||||
}
|
||||
1043
internal/nativeui/ui.go
Normal file
1043
internal/nativeui/ui.go
Normal file
File diff suppressed because it is too large
Load Diff
23
internal/oscmd/command.go
Normal file
23
internal/oscmd/command.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package oscmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// Command creates an exec.Cmd with platform-specific defaults that are safe for
|
||||
// the GUI application. On Windows, the implementation hides child console
|
||||
// windows so helpers like powershell.exe, netsh.exe, route.exe, xray.exe, and
|
||||
// dnstt-client.exe do not flash a terminal window.
|
||||
func Command(name string, args ...string) *exec.Cmd {
|
||||
cmd := exec.Command(name, args...)
|
||||
applyPlatformOptions(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// CommandContext is the context-aware version of Command.
|
||||
func CommandContext(ctx context.Context, name string, args ...string) *exec.Cmd {
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
applyPlatformOptions(cmd)
|
||||
return cmd
|
||||
}
|
||||
7
internal/oscmd/command_other.go
Normal file
7
internal/oscmd/command_other.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build !windows
|
||||
|
||||
package oscmd
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func applyPlatformOptions(cmd *exec.Cmd) {}
|
||||
17
internal/oscmd/command_windows.go
Normal file
17
internal/oscmd/command_windows.go
Normal file
@@ -0,0 +1,17 @@
|
||||
//go:build windows
|
||||
|
||||
package oscmd
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const createNoWindow = 0x08000000
|
||||
|
||||
func applyPlatformOptions(cmd *exec.Cmd) {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
HideWindow: true,
|
||||
CreationFlags: createNoWindow,
|
||||
}
|
||||
}
|
||||
15
internal/platformtun/wintun_other.go
Normal file
15
internal/platformtun/wintun_other.go
Normal file
@@ -0,0 +1,15 @@
|
||||
//go:build !windows
|
||||
|
||||
package platformtun
|
||||
|
||||
import "strings"
|
||||
|
||||
type Logger interface {
|
||||
Add(level, format string, args ...any)
|
||||
}
|
||||
|
||||
func Prepare(device, interfaceName string, mtu int, logger Logger) (string, string, func(), error) {
|
||||
device = strings.TrimSpace(device)
|
||||
interfaceName = strings.TrimSpace(interfaceName)
|
||||
return device, interfaceName, nil, nil
|
||||
}
|
||||
57
internal/platformtun/wintun_windows.go
Normal file
57
internal/platformtun/wintun_windows.go
Normal file
@@ -0,0 +1,57 @@
|
||||
//go:build windows
|
||||
|
||||
package platformtun
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"socksrevivepc/internal/wintunloader"
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
Add(level, format string, args ...any)
|
||||
}
|
||||
|
||||
// Prepare validates and loads Wintun for the current process, then normalizes
|
||||
// the device string used by tun2socks. It intentionally does not pre-open a
|
||||
// WireGuard TUN adapter here: tun2socks must own the live adapter/session.
|
||||
// Pre-opening and closing the adapter before tun2socks can make some Windows
|
||||
// builds close/crash with no useful GUI error.
|
||||
func Prepare(device, interfaceName string, mtu int, logger Logger) (string, string, func(), error) {
|
||||
if mtu <= 0 {
|
||||
mtu = 1500
|
||||
}
|
||||
|
||||
if err := wintunloader.Prepare(logger); err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
device = normalizeWindowsDevice(device)
|
||||
interfaceName = normalizeWindowsInterface(interfaceName)
|
||||
if logger != nil {
|
||||
logger.Add("info", "Windows Wintun is ready: adapter=%s device=%s mtu=%d", interfaceName, device, mtu)
|
||||
}
|
||||
return device, interfaceName, nil, nil
|
||||
}
|
||||
|
||||
func normalizeWindowsDevice(device string) string {
|
||||
device = strings.TrimSpace(device)
|
||||
// tun2socks v2 uses the Windows device model name "wintun". Values
|
||||
// like "wintun://SocksRevive" are Linux-style URL device strings and
|
||||
// can make the engine search for a non-existent network interface.
|
||||
if device == "" || strings.EqualFold(device, "tun") || strings.EqualFold(device, "wintun") {
|
||||
return "wintun"
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(device), "wintun://") || strings.EqualFold(device, "tun://wintun") {
|
||||
return "wintun"
|
||||
}
|
||||
return "wintun"
|
||||
}
|
||||
|
||||
func normalizeWindowsInterface(interfaceName string) string {
|
||||
interfaceName = strings.TrimSpace(interfaceName)
|
||||
if interfaceName == "" || strings.EqualFold(interfaceName, "SocksRevive") {
|
||||
return "wintun"
|
||||
}
|
||||
return interfaceName
|
||||
}
|
||||
574
internal/routes/routes.go
Normal file
574
internal/routes/routes.go
Normal file
@@ -0,0 +1,574 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"socksrevivepc/internal/config"
|
||||
"socksrevivepc/internal/oscmd"
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
Add(level, format string, args ...any)
|
||||
}
|
||||
|
||||
type Cleanup struct {
|
||||
commands [][]string
|
||||
logger Logger
|
||||
}
|
||||
|
||||
func Apply(p config.Profile, proxyHosts []string, logger Logger) (*Cleanup, error) {
|
||||
if !p.Tun.Enabled || !p.Tun.RouteAll {
|
||||
return &Cleanup{logger: logger}, nil
|
||||
}
|
||||
logger.Add("info", "applying TUN routes; admin/root permission may be required")
|
||||
gw, iface, err := defaultGateway()
|
||||
if err != nil {
|
||||
logger.Add("warn", "cannot detect default gateway: %v", err)
|
||||
}
|
||||
|
||||
cleanup := &Cleanup{logger: logger}
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
applyWindowsRoutes(p, proxyHosts, gw, cleanup, logger)
|
||||
case "linux":
|
||||
dev := p.Tun.InterfaceName
|
||||
if dev == "" {
|
||||
dev = "socksrevive0"
|
||||
}
|
||||
_ = run(logger, "ip", "addr", "add", p.Tun.CIDR, "dev", dev)
|
||||
_ = run(logger, "ip", "link", "set", dev, "up")
|
||||
addBypassLinux(proxyHosts, gw, iface, cleanup, logger)
|
||||
if err := run(logger, "ip", "route", "replace", "0.0.0.0/1", "via", p.Tun.Gateway, "dev", dev); err == nil {
|
||||
cleanup.commands = append(cleanup.commands, []string{"ip", "route", "del", "0.0.0.0/1"})
|
||||
}
|
||||
if err := run(logger, "ip", "route", "replace", "128.0.0.0/1", "via", p.Tun.Gateway, "dev", dev); err == nil {
|
||||
cleanup.commands = append(cleanup.commands, []string{"ip", "route", "del", "128.0.0.0/1"})
|
||||
}
|
||||
applyLinuxIPv6Routes(p, proxyHosts, dev, cleanup, logger)
|
||||
case "darwin":
|
||||
dev := p.Tun.InterfaceName
|
||||
_ = run(logger, "ifconfig", dev, p.Tun.Gateway, p.Tun.Gateway, "up")
|
||||
addBypassDarwin(proxyHosts, gw, cleanup, logger)
|
||||
if err := run(logger, "route", "add", "0.0.0.0/1", p.Tun.Gateway); err == nil {
|
||||
cleanup.commands = append(cleanup.commands, []string{"route", "delete", "0.0.0.0/1"})
|
||||
}
|
||||
if err := run(logger, "route", "add", "128.0.0.0/1", p.Tun.Gateway); err == nil {
|
||||
cleanup.commands = append(cleanup.commands, []string{"route", "delete", "128.0.0.0/1"})
|
||||
}
|
||||
applyDarwinIPv6Routes(p, proxyHosts, dev, cleanup, logger)
|
||||
default:
|
||||
logger.Add("warn", "route automation not implemented for %s", runtime.GOOS)
|
||||
}
|
||||
return cleanup, nil
|
||||
}
|
||||
|
||||
func (c *Cleanup) Run() {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
for i := len(c.commands) - 1; i >= 0; i-- {
|
||||
cmd := c.commands[i]
|
||||
if len(cmd) == 0 {
|
||||
continue
|
||||
}
|
||||
_ = run(c.logger, cmd[0], cmd[1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
func applyWindowsRoutes(p config.Profile, proxyHosts []string, gw string, cleanup *Cleanup, logger Logger) {
|
||||
alias, ifIndex, err := waitWindowsTunInterface(p.Tun.InterfaceName, 6*time.Second, logger)
|
||||
if err != nil {
|
||||
logger.Add("warn", "cannot find Windows Wintun adapter for routes: %v", err)
|
||||
return
|
||||
}
|
||||
ip, mask := ipv4CIDRToAddressAndMask(p.Tun.CIDR, p.Tun.Gateway)
|
||||
logger.Add("info", "Windows TUN route adapter: alias=%s ifIndex=%d ip=%s mask=%s", alias, ifIndex, ip, mask)
|
||||
|
||||
_ = run(logger, "netsh", "interface", "ipv4", "set", "interface", fmt.Sprint(ifIndex), "metric=1")
|
||||
if err := run(logger, "netsh", "interface", "ipv4", "set", "address", "name="+alias, "static", ip, mask); err != nil {
|
||||
logger.Add("warn", "failed to set Wintun IPv4 address; routes may not work until Windows finishes creating the adapter")
|
||||
}
|
||||
if len(p.Tun.DNS) > 0 {
|
||||
_ = run(logger, "netsh", "interface", "ipv4", "set", "dnsservers", "name="+alias, "static", p.Tun.DNS[0], "validate=no")
|
||||
for i, dns := range p.Tun.DNS[1:] {
|
||||
dns = strings.TrimSpace(dns)
|
||||
if dns == "" {
|
||||
continue
|
||||
}
|
||||
_ = run(logger, "netsh", "interface", "ipv4", "add", "dnsservers", "name="+alias, dns, fmt.Sprint(i+2), "validate=no")
|
||||
}
|
||||
}
|
||||
|
||||
// Bypass routes must stay on the original physical gateway so the SSH/Xray/DNSTT
|
||||
// control connection never loops back into the TUN default route.
|
||||
addBypassWindows(proxyHosts, gw, cleanup, logger)
|
||||
|
||||
addWindowsIPv4SplitRoute(alias, ifIndex, "0.0.0.0/1", "0.0.0.0", "128.0.0.0", cleanup, logger)
|
||||
addWindowsIPv4SplitRoute(alias, ifIndex, "128.0.0.0/1", "128.0.0.0", "128.0.0.0", cleanup, logger)
|
||||
|
||||
applyWindowsIPv6Routes(p, proxyHosts, alias, ifIndex, cleanup, logger)
|
||||
}
|
||||
|
||||
func applyWindowsIPv6Routes(p config.Profile, proxyHosts []string, alias string, ifIndex int, cleanup *Cleanup, logger Logger) {
|
||||
if !p.Tun.IPv6Enabled && p.Tun.AllowIPv6Leak {
|
||||
logger.Add("info", "IPv6 TUN is disabled and IPv6 leak blocking is off; leaving normal IPv6 routes untouched")
|
||||
return
|
||||
}
|
||||
|
||||
_ = run(logger, "netsh", "interface", "ipv6", "set", "interface", fmt.Sprint(ifIndex), "metric=1")
|
||||
ipv6Addr, prefixLen := ipv6CIDRToAddressAndPrefix(p.Tun.IPv6CIDR)
|
||||
if p.Tun.IPv6Enabled {
|
||||
logger.Add("info", "IPv6 TUN routing enabled: address=%s/%s", ipv6Addr, prefixLen)
|
||||
_ = run(logger, "netsh", "interface", "ipv6", "add", "address", "interface="+alias, "address="+ipv6Addr, "store=active")
|
||||
setWindowsIPv6DNS(alias, p.Tun.IPv6DNS, logger)
|
||||
if gw, idxStr, err := defaultGatewayIPv6(); err == nil {
|
||||
if idx, convErr := strconv.Atoi(idxStr); convErr == nil && idx > 0 {
|
||||
addBypassWindowsIPv6(proxyHosts, gw, idx, cleanup, logger)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.Add("info", "IPv6 TUN is disabled; adding IPv6 split routes as leak protection")
|
||||
}
|
||||
addWindowsIPv6SplitRoute(alias, "::/1", cleanup, logger)
|
||||
addWindowsIPv6SplitRoute(alias, "8000::/1", cleanup, logger)
|
||||
}
|
||||
|
||||
func setWindowsIPv6DNS(alias string, dns []string, logger Logger) {
|
||||
if len(dns) == 0 {
|
||||
return
|
||||
}
|
||||
first := strings.TrimSpace(dns[0])
|
||||
if first == "" {
|
||||
return
|
||||
}
|
||||
_ = run(logger, "netsh", "interface", "ipv6", "set", "dnsservers", "name="+alias, "static", first, "validate=no")
|
||||
for i, server := range dns[1:] {
|
||||
server = strings.TrimSpace(server)
|
||||
if server == "" {
|
||||
continue
|
||||
}
|
||||
_ = run(logger, "netsh", "interface", "ipv6", "add", "dnsservers", "name="+alias, server, fmt.Sprint(i+2), "validate=no")
|
||||
}
|
||||
}
|
||||
|
||||
func applyLinuxIPv6Routes(p config.Profile, proxyHosts []string, dev string, cleanup *Cleanup, logger Logger) {
|
||||
if !p.Tun.IPv6Enabled && p.Tun.AllowIPv6Leak {
|
||||
logger.Add("info", "IPv6 TUN is disabled and IPv6 leak blocking is off; leaving normal IPv6 routes untouched")
|
||||
return
|
||||
}
|
||||
if p.Tun.IPv6Enabled {
|
||||
logger.Add("info", "IPv6 TUN routing enabled: cidr=%s", p.Tun.IPv6CIDR)
|
||||
_ = run(logger, "ip", "-6", "addr", "add", p.Tun.IPv6CIDR, "dev", dev)
|
||||
if gw, iface, err := defaultGatewayIPv6(); err == nil {
|
||||
addBypassLinuxIPv6(proxyHosts, gw, iface, cleanup, logger)
|
||||
}
|
||||
} else {
|
||||
logger.Add("info", "IPv6 TUN is disabled; adding IPv6 split routes as leak protection")
|
||||
}
|
||||
if err := run(logger, "ip", "-6", "route", "replace", "::/1", "dev", dev, "metric", "1"); err == nil {
|
||||
cleanup.commands = append(cleanup.commands, []string{"ip", "-6", "route", "del", "::/1"})
|
||||
}
|
||||
if err := run(logger, "ip", "-6", "route", "replace", "8000::/1", "dev", dev, "metric", "1"); err == nil {
|
||||
cleanup.commands = append(cleanup.commands, []string{"ip", "-6", "route", "del", "8000::/1"})
|
||||
}
|
||||
}
|
||||
|
||||
func applyDarwinIPv6Routes(p config.Profile, proxyHosts []string, dev string, cleanup *Cleanup, logger Logger) {
|
||||
if !p.Tun.IPv6Enabled && p.Tun.AllowIPv6Leak {
|
||||
logger.Add("info", "IPv6 TUN is disabled and IPv6 leak blocking is off; leaving normal IPv6 routes untouched")
|
||||
return
|
||||
}
|
||||
ipv6Addr, prefixLen := ipv6CIDRToAddressAndPrefix(p.Tun.IPv6CIDR)
|
||||
if p.Tun.IPv6Enabled {
|
||||
logger.Add("info", "IPv6 TUN routing enabled: address=%s/%s", ipv6Addr, prefixLen)
|
||||
_ = run(logger, "ifconfig", dev, "inet6", ipv6Addr, "prefixlen", prefixLen, "up")
|
||||
if gw, _, err := defaultGatewayIPv6(); err == nil {
|
||||
addBypassDarwinIPv6(proxyHosts, gw, cleanup, logger)
|
||||
}
|
||||
} else {
|
||||
logger.Add("info", "IPv6 TUN is disabled; adding IPv6 split routes as leak protection")
|
||||
}
|
||||
if err := run(logger, "route", "add", "-inet6", "::/1", "-interface", dev); err == nil {
|
||||
cleanup.commands = append(cleanup.commands, []string{"route", "delete", "-inet6", "::/1"})
|
||||
}
|
||||
if err := run(logger, "route", "add", "-inet6", "8000::/1", "-interface", dev); err == nil {
|
||||
cleanup.commands = append(cleanup.commands, []string{"route", "delete", "-inet6", "8000::/1"})
|
||||
}
|
||||
}
|
||||
|
||||
func addWindowsIPv4SplitRoute(alias string, ifIndex int, prefix, legacyDest, legacyMask string, cleanup *Cleanup, logger Logger) {
|
||||
args := []string{"interface", "ipv4", "add", "route", "prefix=" + prefix, "interface=" + alias, "nexthop=0.0.0.0", "metric=1", "store=active"}
|
||||
if err := run(logger, "netsh", args...); err == nil {
|
||||
cleanup.commands = append(cleanup.commands, []string{"netsh", "interface", "ipv4", "delete", "route", "prefix=" + prefix, "interface=" + alias, "nexthop=0.0.0.0", "store=active"})
|
||||
return
|
||||
}
|
||||
// Fallback for older Windows/netsh variants.
|
||||
if err := run(logger, "route", "add", legacyDest, "mask", legacyMask, "0.0.0.0", "metric", "1", "if", fmt.Sprint(ifIndex)); err == nil {
|
||||
cleanup.commands = append(cleanup.commands, []string{"route", "delete", legacyDest, "mask", legacyMask})
|
||||
}
|
||||
}
|
||||
|
||||
func addWindowsIPv6SplitRoute(alias, prefix string, cleanup *Cleanup, logger Logger) {
|
||||
args := []string{"interface", "ipv6", "add", "route", "prefix=" + prefix, "interface=" + alias, "nexthop=::", "metric=1", "store=active"}
|
||||
if err := run(logger, "netsh", args...); err == nil {
|
||||
cleanup.commands = append(cleanup.commands, []string{"netsh", "interface", "ipv6", "delete", "route", "prefix=" + prefix, "interface=" + alias, "nexthop=::", "store=active"})
|
||||
}
|
||||
}
|
||||
|
||||
func ipv4CIDRToAddressAndMask(cidr, fallbackIP string) (string, string) {
|
||||
ip, ipnet, err := net.ParseCIDR(strings.TrimSpace(cidr))
|
||||
if err == nil && ip.To4() != nil {
|
||||
return ip.String(), net.IP(ipnet.Mask).String()
|
||||
}
|
||||
if parsed := net.ParseIP(strings.TrimSpace(fallbackIP)); parsed != nil && parsed.To4() != nil {
|
||||
return parsed.String(), "255.254.0.0"
|
||||
}
|
||||
return "198.18.0.1", "255.254.0.0"
|
||||
}
|
||||
|
||||
func ipv6CIDRToAddressAndPrefix(cidr string) (string, string) {
|
||||
ip, ipnet, err := net.ParseCIDR(strings.TrimSpace(cidr))
|
||||
if err == nil && ip.To4() == nil && ip.To16() != nil {
|
||||
ones, _ := ipnet.Mask.Size()
|
||||
return ip.String(), fmt.Sprint(ones)
|
||||
}
|
||||
return "fd00:534f:434b::1", "64"
|
||||
}
|
||||
|
||||
type windowsAdapter struct {
|
||||
Name string `json:"Name"`
|
||||
InterfaceAlias string `json:"InterfaceAlias"`
|
||||
InterfaceIndex int `json:"InterfaceIndex"`
|
||||
IfIndex int `json:"ifIndex"`
|
||||
Status string `json:"Status"`
|
||||
InterfaceDesc string `json:"InterfaceDescription"`
|
||||
}
|
||||
|
||||
func waitWindowsTunInterface(preferred string, timeout time.Duration, logger Logger) (string, int, error) {
|
||||
deadline := time.Now().Add(timeout)
|
||||
var lastErr error
|
||||
for time.Now().Before(deadline) {
|
||||
alias, idx, err := windowsTunInterface(preferred)
|
||||
if err == nil && alias != "" && idx > 0 {
|
||||
return alias, idx, nil
|
||||
}
|
||||
lastErr = err
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}
|
||||
if lastErr != nil {
|
||||
return "", 0, lastErr
|
||||
}
|
||||
return "", 0, fmt.Errorf("not found")
|
||||
}
|
||||
|
||||
func windowsTunInterface(preferred string) (string, int, error) {
|
||||
preferred = strings.TrimSpace(preferred)
|
||||
if preferred == "" {
|
||||
preferred = "wintun"
|
||||
}
|
||||
ps := `$preferred = ` + strconv.Quote(preferred) + `
|
||||
$adapters = @(Get-NetAdapter -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.Status -ne 'Disabled' -and (
|
||||
$_.Name -ieq $preferred -or
|
||||
$_.InterfaceDescription -like '*Wintun*' -or
|
||||
$_.InterfaceDescription -like '*WireGuard*' -or
|
||||
$_.Name -like '*wintun*'
|
||||
)
|
||||
} | Sort-Object @{Expression={if ($_.Name -ieq $preferred) {0} else {1}}}, InterfaceIndex | Select-Object -First 1 Name,InterfaceDescription,InterfaceIndex,ifIndex,Status)
|
||||
if ($adapters.Count -eq 0) { exit 2 }
|
||||
$adapters[0] | ConvertTo-Json -Compress`
|
||||
out, err := oscmd.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps).Output()
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
var a windowsAdapter
|
||||
if err := json.Unmarshal(out, &a); err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
idx := a.InterfaceIndex
|
||||
if idx == 0 {
|
||||
idx = a.IfIndex
|
||||
}
|
||||
name := a.Name
|
||||
if name == "" {
|
||||
name = a.InterfaceAlias
|
||||
}
|
||||
if name == "" || idx == 0 {
|
||||
return "", 0, fmt.Errorf("invalid adapter json %q", strings.TrimSpace(string(out)))
|
||||
}
|
||||
return name, idx, nil
|
||||
}
|
||||
|
||||
func addBypassWindows(hosts []string, gw string, cleanup *Cleanup, logger Logger) {
|
||||
if gw == "" {
|
||||
return
|
||||
}
|
||||
for _, ip := range resolveIPv4(hosts) {
|
||||
if err := run(logger, "route", "add", ip, "mask", "255.255.255.255", gw, "metric", "1"); err == nil {
|
||||
cleanup.commands = append(cleanup.commands, []string{"route", "delete", ip, "mask", "255.255.255.255"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addBypassLinux(hosts []string, gw, iface string, cleanup *Cleanup, logger Logger) {
|
||||
if gw == "" {
|
||||
return
|
||||
}
|
||||
for _, ip := range resolveIPv4(hosts) {
|
||||
args := []string{"route", "add", ip + "/32", "via", gw}
|
||||
if iface != "" {
|
||||
args = append(args, "dev", iface)
|
||||
}
|
||||
if err := run(logger, "ip", args...); err == nil {
|
||||
cleanup.commands = append(cleanup.commands, []string{"ip", "route", "del", ip + "/32"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addBypassDarwin(hosts []string, gw string, cleanup *Cleanup, logger Logger) {
|
||||
if gw == "" {
|
||||
return
|
||||
}
|
||||
for _, ip := range resolveIPv4(hosts) {
|
||||
if err := run(logger, "route", "add", "-host", ip, gw); err == nil {
|
||||
cleanup.commands = append(cleanup.commands, []string{"route", "delete", "-host", ip})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addBypassWindowsIPv6(hosts []string, gw string, ifIndex int, cleanup *Cleanup, logger Logger) {
|
||||
if gw == "" || ifIndex == 0 {
|
||||
return
|
||||
}
|
||||
for _, ip := range resolveIPv6(hosts) {
|
||||
prefix := ip + "/128"
|
||||
if err := run(logger, "netsh", "interface", "ipv6", "add", "route", "prefix="+prefix, "interface="+fmt.Sprint(ifIndex), "nexthop="+gw, "metric=1", "store=active"); err == nil {
|
||||
cleanup.commands = append(cleanup.commands, []string{"netsh", "interface", "ipv6", "delete", "route", "prefix=" + prefix, "interface=" + fmt.Sprint(ifIndex), "nexthop=" + gw, "store=active"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addBypassLinuxIPv6(hosts []string, gw, iface string, cleanup *Cleanup, logger Logger) {
|
||||
for _, ip := range resolveIPv6(hosts) {
|
||||
args := []string{"-6", "route", "add", ip + "/128"}
|
||||
if gw != "" {
|
||||
args = append(args, "via", gw)
|
||||
}
|
||||
if iface != "" {
|
||||
args = append(args, "dev", iface)
|
||||
}
|
||||
if err := run(logger, "ip", args...); err == nil {
|
||||
cleanup.commands = append(cleanup.commands, []string{"ip", "-6", "route", "del", ip + "/128"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addBypassDarwinIPv6(hosts []string, gw string, cleanup *Cleanup, logger Logger) {
|
||||
if gw == "" {
|
||||
return
|
||||
}
|
||||
for _, ip := range resolveIPv6(hosts) {
|
||||
if err := run(logger, "route", "add", "-inet6", "-host", ip, gw); err == nil {
|
||||
cleanup.commands = append(cleanup.commands, []string{"route", "delete", "-inet6", "-host", ip})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resolveIPv6(hosts []string) []string {
|
||||
seen := map[string]bool{}
|
||||
var out []string
|
||||
for _, host := range hosts {
|
||||
host = strings.TrimSpace(host)
|
||||
if host == "" {
|
||||
continue
|
||||
}
|
||||
if h, _, err := net.SplitHostPort(host); err == nil {
|
||||
host = h
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
if ip.To4() == nil && ip.To16() != nil && !seen[ip.String()] {
|
||||
seen[ip.String()] = true
|
||||
out = append(out, ip.String())
|
||||
}
|
||||
continue
|
||||
}
|
||||
ips, err := net.LookupIP(host)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, ip := range ips {
|
||||
if ip.To4() == nil && ip.To16() != nil && !seen[ip.String()] {
|
||||
seen[ip.String()] = true
|
||||
out = append(out, ip.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func resolveIPv4(hosts []string) []string {
|
||||
seen := map[string]bool{}
|
||||
var out []string
|
||||
for _, host := range hosts {
|
||||
host = strings.TrimSpace(host)
|
||||
if host == "" || net.ParseIP(host) != nil && net.ParseIP(host).To4() == nil {
|
||||
continue
|
||||
}
|
||||
ips, err := net.LookupIP(host)
|
||||
if err != nil {
|
||||
if ip := net.ParseIP(host); ip != nil && ip.To4() != nil && !seen[ip.String()] {
|
||||
seen[ip.String()] = true
|
||||
out = append(out, ip.String())
|
||||
}
|
||||
continue
|
||||
}
|
||||
for _, ip := range ips {
|
||||
v4 := ip.To4()
|
||||
if v4 != nil && !seen[v4.String()] {
|
||||
seen[v4.String()] = true
|
||||
out = append(out, v4.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func defaultGatewayIPv6() (gateway string, iface string, err error) {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
ps := `Get-NetRoute -AddressFamily IPv6 -DestinationPrefix "::/0" -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.NextHop -and $_.NextHop -ne "::" } |
|
||||
Sort-Object RouteMetric, InterfaceMetric |
|
||||
Select-Object -First 1 NextHop, InterfaceIndex | ConvertTo-Json -Compress`
|
||||
out, err := oscmd.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps).Output()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
var row struct {
|
||||
NextHop string `json:"NextHop"`
|
||||
InterfaceIndex int `json:"InterfaceIndex"`
|
||||
}
|
||||
if err := json.Unmarshal(out, &row); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if row.NextHop == "" || row.InterfaceIndex == 0 {
|
||||
return "", "", fmt.Errorf("IPv6 default gateway not found")
|
||||
}
|
||||
return row.NextHop, fmt.Sprint(row.InterfaceIndex), nil
|
||||
case "linux":
|
||||
out, err := oscmd.Command("ip", "-6", "route", "show", "default").Output()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
fields := strings.Fields(string(out))
|
||||
for i, f := range fields {
|
||||
if f == "via" && i+1 < len(fields) {
|
||||
gateway = fields[i+1]
|
||||
}
|
||||
if f == "dev" && i+1 < len(fields) {
|
||||
iface = fields[i+1]
|
||||
}
|
||||
}
|
||||
if gateway == "" && iface == "" {
|
||||
return "", "", fmt.Errorf("IPv6 default gateway not found")
|
||||
}
|
||||
return gateway, iface, nil
|
||||
case "darwin":
|
||||
out, err := oscmd.Command("route", "-n", "get", "-inet6", "default").Output()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "gateway:") {
|
||||
gateway = strings.TrimSpace(strings.TrimPrefix(line, "gateway:"))
|
||||
}
|
||||
if strings.HasPrefix(line, "interface:") {
|
||||
iface = strings.TrimSpace(strings.TrimPrefix(line, "interface:"))
|
||||
}
|
||||
}
|
||||
if gateway == "" && iface == "" {
|
||||
return "", "", fmt.Errorf("IPv6 default gateway not found")
|
||||
}
|
||||
return gateway, iface, nil
|
||||
default:
|
||||
return "", "", fmt.Errorf("IPv6 default gateway detection not implemented for %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
func run(logger Logger, name string, args ...string) error {
|
||||
cmd := oscmd.Command(name, args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
line := strings.TrimSpace(string(out))
|
||||
if err != nil {
|
||||
logger.Add("warn", "%s %s failed: %v %s", name, strings.Join(args, " "), err, line)
|
||||
return err
|
||||
}
|
||||
if line != "" {
|
||||
logger.Add("debug", "%s: %s", name, line)
|
||||
} else if strings.EqualFold(name, "netsh") || strings.EqualFold(name, "route") || strings.EqualFold(name, "ip") {
|
||||
logger.Add("debug", "%s %s: OK", name, strings.Join(args, " "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func defaultGateway() (gateway string, iface string, err error) {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
out, err := oscmd.Command("ip", "route", "show", "default").Output()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
fields := strings.Fields(string(out))
|
||||
for i, f := range fields {
|
||||
if f == "via" && i+1 < len(fields) {
|
||||
gateway = fields[i+1]
|
||||
}
|
||||
if f == "dev" && i+1 < len(fields) {
|
||||
iface = fields[i+1]
|
||||
}
|
||||
}
|
||||
return gateway, iface, nil
|
||||
case "darwin":
|
||||
out, err := oscmd.Command("route", "-n", "get", "default").Output()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "gateway:") {
|
||||
gateway = strings.TrimSpace(strings.TrimPrefix(line, "gateway:"))
|
||||
}
|
||||
if strings.HasPrefix(line, "interface:") {
|
||||
iface = strings.TrimSpace(strings.TrimPrefix(line, "interface:"))
|
||||
}
|
||||
}
|
||||
return gateway, iface, nil
|
||||
case "windows":
|
||||
ps := `Get-NetRoute -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.NextHop -and $_.NextHop -ne '0.0.0.0' } |
|
||||
Sort-Object RouteMetric, InterfaceMetric |
|
||||
Select-Object -First 1 NextHop,InterfaceAlias |
|
||||
ConvertTo-Json -Compress`
|
||||
out, err := oscmd.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps).Output()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
var r struct {
|
||||
NextHop string
|
||||
InterfaceAlias string
|
||||
}
|
||||
if err := json.Unmarshal(out, &r); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return r.NextHop, r.InterfaceAlias, nil
|
||||
default:
|
||||
return "", "", fmt.Errorf("unsupported OS")
|
||||
}
|
||||
}
|
||||
96
internal/tun/tun2socks.go
Normal file
96
internal/tun/tun2socks.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/xjasonlyu/tun2socks/v2/engine"
|
||||
"socksrevivepc/internal/config"
|
||||
"socksrevivepc/internal/platformtun"
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
Add(level, format string, args ...any)
|
||||
}
|
||||
|
||||
type Runner struct {
|
||||
active bool
|
||||
logger Logger
|
||||
cleanup func()
|
||||
}
|
||||
|
||||
func NewRunner(logger Logger) *Runner { return &Runner{logger: logger} }
|
||||
|
||||
func (r *Runner) Start(p *config.Profile, socksAddr string) error {
|
||||
if p == nil || !p.Tun.Enabled {
|
||||
return nil
|
||||
}
|
||||
if r.active {
|
||||
return fmt.Errorf("tun is already active")
|
||||
}
|
||||
if err := waitForProxyPort(socksAddr, 2500*time.Millisecond); err != nil {
|
||||
return fmt.Errorf("cannot start TUN because upstream SOCKS is not reachable at %s: %w", socksAddr, err)
|
||||
}
|
||||
device, iface, cleanup, err := platformtun.Prepare(p.Tun.Device, p.Tun.InterfaceName, p.Tun.MTU, r.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Tun.Device = device
|
||||
p.Tun.InterfaceName = iface
|
||||
r.cleanup = cleanup
|
||||
key := &engine.Key{
|
||||
MTU: p.Tun.MTU,
|
||||
Device: p.Tun.Device,
|
||||
Proxy: "socks5://" + socksAddr,
|
||||
LogLevel: "error",
|
||||
}
|
||||
if p.Tun.InterfaceName != "" && runtime.GOOS != "windows" {
|
||||
key.Interface = p.Tun.InterfaceName
|
||||
}
|
||||
if runtime.GOOS == "windows" && r.logger != nil {
|
||||
r.logger.Add("info", "Windows TUN: leaving tun2socks interface binding empty; the TUN adapter name is %s", p.Tun.InterfaceName)
|
||||
}
|
||||
engine.Insert(key)
|
||||
// tun2socks/v2 engine.Start() initializes the default netstack and then
|
||||
// returns; it does not block for the lifetime of the tunnel. The stack stays
|
||||
// alive globally until engine.Stop() is called. Older builds treated this
|
||||
// normal return as "engine exited immediately", which made TUN mode fail even
|
||||
// after the stack was correctly created.
|
||||
engine.Start()
|
||||
r.active = true
|
||||
r.logger.Add("info", "tun2socks started: device=%s interface=%s proxy=socks5://%s", p.Tun.Device, p.Tun.InterfaceName, socksAddr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Runner) Stop() {
|
||||
if !r.active {
|
||||
return
|
||||
}
|
||||
engine.Stop()
|
||||
if r.cleanup != nil {
|
||||
r.cleanup()
|
||||
r.cleanup = nil
|
||||
}
|
||||
r.active = false
|
||||
r.logger.Add("info", "tun2socks stopped")
|
||||
}
|
||||
|
||||
func waitForProxyPort(addr string, timeout time.Duration) error {
|
||||
deadline := time.Now().Add(timeout)
|
||||
var lastErr error
|
||||
for time.Now().Before(deadline) {
|
||||
c, err := net.DialTimeout("tcp", addr, 350*time.Millisecond)
|
||||
if err == nil {
|
||||
_ = c.Close()
|
||||
return nil
|
||||
}
|
||||
lastErr = err
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
}
|
||||
if lastErr != nil {
|
||||
return lastErr
|
||||
}
|
||||
return fmt.Errorf("timeout waiting for %s", addr)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
Put the official amd64 wintun.dll here and rebuild if you want SocksRevivePC.exe to embed Wintun internally.
|
||||
Expected filename:
|
||||
|
||||
wintun.dll
|
||||
|
||||
Use the signed DLL from the official Wintun package.
|
||||
BIN
internal/wintunloader/assets/wintun/windows/amd64/wintun.dll
Normal file
BIN
internal/wintunloader/assets/wintun/windows/amd64/wintun.dll
Normal file
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
Put the official arm64 wintun.dll here and rebuild if you build GOARCH=arm64.
|
||||
Expected filename:
|
||||
|
||||
wintun.dll
|
||||
|
||||
Use the signed DLL from the official Wintun package.
|
||||
318
internal/wintunloader/loader_windows.go
Normal file
318
internal/wintunloader/loader_windows.go
Normal file
@@ -0,0 +1,318 @@
|
||||
//go:build windows
|
||||
|
||||
package wintunloader
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
//go:embed assets/wintun/windows/amd64/* assets/wintun/windows/arm64/*
|
||||
var embeddedWintun embed.FS
|
||||
|
||||
type Logger interface {
|
||||
Add(level, format string, args ...any)
|
||||
}
|
||||
|
||||
// Prepare makes Wintun available before tun2socks/WireGuard tries to load it.
|
||||
// Windows cannot create a real TUN adapter without the signed wintun.dll layer,
|
||||
// so this function validates the DLL architecture, extracts embedded assets when
|
||||
// present, and updates the current process DLL search path.
|
||||
func Prepare(logger Logger) error {
|
||||
arch := runtime.GOARCH
|
||||
if arch != "amd64" && arch != "arm64" {
|
||||
return fmt.Errorf("Wintun embedded loader only supports amd64/arm64, current GOARCH=%s", arch)
|
||||
}
|
||||
|
||||
exeDir := executableDir()
|
||||
workDir := workingDir()
|
||||
|
||||
candidateDirs := uniqueNonEmpty([]string{
|
||||
exeDir,
|
||||
workDir,
|
||||
filepath.Join(exeDir, "tools", "wintun", arch),
|
||||
filepath.Join(workDir, "tools", "wintun", arch),
|
||||
filepath.Join(exeDir, "tools", "wintun"),
|
||||
filepath.Join(workDir, "tools", "wintun"),
|
||||
})
|
||||
|
||||
for _, dir := range candidateDirs {
|
||||
path := filepath.Join(dir, "wintun.dll")
|
||||
if ok, reason := validDLLForArch(path, arch); ok {
|
||||
installDir, installPath, err := ensureProcessDLL(path, exeDir, arch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
activateDLLDirectory(installDir, append(candidateDirs, installDir)...)
|
||||
if logger != nil {
|
||||
logger.Add("info", "Wintun DLL ready: %s", installPath)
|
||||
}
|
||||
return nil
|
||||
} else if fileExists(path) && logger != nil {
|
||||
logger.Add("warn", "Ignoring invalid Wintun DLL at %s: %s", path, reason)
|
||||
}
|
||||
}
|
||||
|
||||
embeddedPath := filepath.ToSlash(filepath.Join("assets", "wintun", "windows", arch, "wintun.dll"))
|
||||
data, err := embeddedWintun.ReadFile(embeddedPath)
|
||||
if err == nil {
|
||||
if err := validateDLLBytes(data, arch); err != nil {
|
||||
return fmt.Errorf("embedded wintun.dll is invalid for %s: %w", arch, err)
|
||||
}
|
||||
installDir, installPath, err := writeEmbeddedDLL(data, exeDir, arch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
activateDLLDirectory(installDir, append(candidateDirs, installDir)...)
|
||||
if logger != nil {
|
||||
logger.Add("info", "Embedded Wintun extracted: %s", installPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
return fmt.Errorf("read embedded wintun.dll failed: %w", err)
|
||||
}
|
||||
|
||||
activateDLLDirectory("", candidateDirs...)
|
||||
return fmt.Errorf("wintun.dll was not found. The TUN engine is compiled into the app, but Windows still requires the signed Wintun DLL. Put the official %s wintun.dll at tools/wintun/%s/wintun.dll, run scripts/embed_wintun_from_tools.ps1, and rebuild", arch, arch)
|
||||
}
|
||||
|
||||
func ensureProcessDLL(source, exeDir, arch string) (string, string, error) {
|
||||
if ok, reason := validDLLForArch(source, arch); !ok {
|
||||
return "", "", fmt.Errorf("source Wintun DLL is invalid: %s: %s", source, reason)
|
||||
}
|
||||
if source == filepath.Join(exeDir, "wintun.dll") {
|
||||
return exeDir, source, nil
|
||||
}
|
||||
installDir := preferredInstallDir(exeDir, arch)
|
||||
installPath := filepath.Join(installDir, "wintun.dll")
|
||||
if samePath(source, installPath) {
|
||||
return installDir, installPath, nil
|
||||
}
|
||||
if ok, _ := validDLLForArch(installPath, arch); ok {
|
||||
return installDir, installPath, nil
|
||||
}
|
||||
if err := copyFile(source, installPath); err != nil {
|
||||
// Fall back to the source directory if we cannot copy beside the exe.
|
||||
return filepath.Dir(source), source, nil
|
||||
}
|
||||
return installDir, installPath, nil
|
||||
}
|
||||
|
||||
func writeEmbeddedDLL(data []byte, exeDir, arch string) (string, string, error) {
|
||||
if err := validateDLLBytes(data, arch); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
installDir := preferredInstallDir(exeDir, arch)
|
||||
installPath := filepath.Join(installDir, "wintun.dll")
|
||||
if ok, _ := validDLLForArch(installPath, arch); ok {
|
||||
return installDir, installPath, nil
|
||||
}
|
||||
if err := os.MkdirAll(installDir, 0o755); err != nil {
|
||||
return "", "", fmt.Errorf("create Wintun install directory failed: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(installPath, data, 0o644); err != nil {
|
||||
cacheDir := filepath.Join(userCacheDir(), "SocksRevivePC", "wintun", arch)
|
||||
cachePath := filepath.Join(cacheDir, "wintun.dll")
|
||||
if err2 := os.MkdirAll(cacheDir, 0o755); err2 != nil {
|
||||
return "", "", fmt.Errorf("write embedded Wintun failed: %w; cache fallback mkdir failed: %v", err, err2)
|
||||
}
|
||||
if err2 := os.WriteFile(cachePath, data, 0o644); err2 != nil {
|
||||
return "", "", fmt.Errorf("write embedded Wintun failed: %w; cache fallback write failed: %v", err, err2)
|
||||
}
|
||||
return cacheDir, cachePath, nil
|
||||
}
|
||||
return installDir, installPath, nil
|
||||
}
|
||||
|
||||
func preferredInstallDir(exeDir, arch string) string {
|
||||
if exeDir != "" {
|
||||
return exeDir
|
||||
}
|
||||
return filepath.Join(userCacheDir(), "SocksRevivePC", "wintun", arch)
|
||||
}
|
||||
|
||||
func executableDir() string {
|
||||
exe, err := os.Executable()
|
||||
if err != nil || exe == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Dir(exe)
|
||||
}
|
||||
|
||||
func workingDir() string {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return wd
|
||||
}
|
||||
|
||||
func userCacheDir() string {
|
||||
dir, err := os.UserCacheDir()
|
||||
if err != nil || dir == "" {
|
||||
return os.TempDir()
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func activateDLLDirectory(primary string, dirs ...string) {
|
||||
if primary != "" {
|
||||
_ = setDLLDirectory(primary)
|
||||
}
|
||||
prependPath(dirs...)
|
||||
}
|
||||
|
||||
func setDLLDirectory(dir string) error {
|
||||
if strings.TrimSpace(dir) == "" {
|
||||
return nil
|
||||
}
|
||||
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||
proc := kernel32.NewProc("SetDllDirectoryW")
|
||||
ptr, err := syscall.UTF16PtrFromString(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r1, _, callErr := proc.Call(uintptr(unsafe.Pointer(ptr)))
|
||||
if r1 == 0 {
|
||||
return callErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func prependPath(dirs ...string) {
|
||||
dirs = uniqueNonEmpty(dirs)
|
||||
if len(dirs) == 0 {
|
||||
return
|
||||
}
|
||||
current := os.Getenv("PATH")
|
||||
parts := strings.Split(current, string(os.PathListSeparator))
|
||||
var cleaned []string
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" && !containsPath(dirs, p) {
|
||||
cleaned = append(cleaned, p)
|
||||
}
|
||||
}
|
||||
os.Setenv("PATH", strings.Join(append(dirs, cleaned...), string(os.PathListSeparator)))
|
||||
}
|
||||
|
||||
func uniqueNonEmpty(in []string) []string {
|
||||
out := make([]string, 0, len(in))
|
||||
for _, s := range in {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
if !containsPath(out, s) {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func containsPath(paths []string, p string) bool {
|
||||
for _, existing := range paths {
|
||||
if samePath(existing, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func samePath(a, b string) bool {
|
||||
if a == "" || b == "" {
|
||||
return false
|
||||
}
|
||||
return strings.EqualFold(filepath.Clean(a), filepath.Clean(b))
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
st, err := os.Stat(path)
|
||||
return err == nil && !st.IsDir()
|
||||
}
|
||||
|
||||
func validDLLForArch(path, arch string) (bool, string) {
|
||||
st, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
if st.IsDir() {
|
||||
return false, "path is a directory"
|
||||
}
|
||||
if st.Size() <= 1024 {
|
||||
return false, "file is too small"
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
if err := validateDLLBytes(data, arch); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func validateDLLBytes(data []byte, arch string) error {
|
||||
if len(data) <= 1024 {
|
||||
return fmt.Errorf("file is too small")
|
||||
}
|
||||
if len(data) < 0x40 || data[0] != 'M' || data[1] != 'Z' {
|
||||
return fmt.Errorf("not a Windows PE DLL")
|
||||
}
|
||||
peOffset := int(binary.LittleEndian.Uint32(data[0x3c:0x40]))
|
||||
if peOffset <= 0 || len(data) < peOffset+6 {
|
||||
return fmt.Errorf("invalid PE header")
|
||||
}
|
||||
if string(data[peOffset:peOffset+4]) != "PE\x00\x00" {
|
||||
return fmt.Errorf("missing PE signature")
|
||||
}
|
||||
machine := binary.LittleEndian.Uint16(data[peOffset+4 : peOffset+6])
|
||||
want := uint16(0x8664) // IMAGE_FILE_MACHINE_AMD64
|
||||
if arch == "arm64" {
|
||||
want = 0xaa64 // IMAGE_FILE_MACHINE_ARM64
|
||||
}
|
||||
if machine != want {
|
||||
return fmt.Errorf("wrong architecture machine=0x%04x expected=0x%04x for %s", machine, want, arch)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFile(source, target string) error {
|
||||
if ok, reason := validDLLForArch(source, runtime.GOARCH); !ok {
|
||||
return fmt.Errorf("source DLL is missing or invalid: %s: %s", source, reason)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
in, err := os.Open(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
out, err := os.Create(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, copyErr := io.Copy(out, in)
|
||||
closeErr := out.Close()
|
||||
if copyErr != nil {
|
||||
_ = os.Remove(target)
|
||||
return copyErr
|
||||
}
|
||||
if closeErr != nil {
|
||||
_ = os.Remove(target)
|
||||
return closeErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
0
logs/.gitkeep
Normal file
0
logs/.gitkeep
Normal file
0
profiles/.gitkeep
Normal file
0
profiles/.gitkeep
Normal file
BIN
profiles/fb2c4da98c538260.srpc
Normal file
BIN
profiles/fb2c4da98c538260.srpc
Normal file
Binary file not shown.
8
scripts/build_linux.sh
Normal file
8
scripts/build_linux.sh
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
mkdir -p dist
|
||||
export CGO_ENABLED=1
|
||||
go mod tidy
|
||||
go build -trimpath -ldflags "-s -w" -o dist/socksrevivepc ./cmd/socksrevivepc
|
||||
echo "Built dist/socksrevivepc"
|
||||
echo "Run with sudo when using TUN mode."
|
||||
112
scripts/build_windows.ps1
Normal file
112
scripts/build_windows.ps1
Normal file
@@ -0,0 +1,112 @@
|
||||
param(
|
||||
[switch]$NoTidy,
|
||||
[switch]$Debug
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Write-Info($Message) {
|
||||
Write-Host "[SocksRevivePC] $Message"
|
||||
}
|
||||
|
||||
function Add-PathIfExists($PathToAdd) {
|
||||
if ((Test-Path $PathToAdd) -and (($env:PATH -split ';') -notcontains $PathToAdd)) {
|
||||
$env:PATH = "$PathToAdd;$env:PATH"
|
||||
Write-Info "Added to PATH for this build: $PathToAdd"
|
||||
}
|
||||
}
|
||||
|
||||
function Require-Command($Name, $InstallHint) {
|
||||
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
|
||||
throw "$Name was not found in PATH. $InstallHint"
|
||||
}
|
||||
}
|
||||
|
||||
function Run-Native($Exe, $Arguments) {
|
||||
Write-Info "$Exe $($Arguments -join ' ')"
|
||||
& $Exe @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "$Exe failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
}
|
||||
|
||||
# Fyne uses CGO on Windows. Try common MSYS2 MinGW locations before failing.
|
||||
Add-PathIfExists "C:\msys64\ucrt64\bin"
|
||||
Add-PathIfExists "C:\msys64\mingw64\bin"
|
||||
Add-PathIfExists "C:\msys64\clang64\bin"
|
||||
|
||||
Require-Command "go" "Install Go 1.22+ and reopen PowerShell."
|
||||
Require-Command "gcc" "Install MSYS2 and run: pacman -S --needed mingw-w64-ucrt-x86_64-gcc"
|
||||
Require-Command "windres" "Install MSYS2 windres/binutils: pacman -S --needed mingw-w64-ucrt-x86_64-binutils"
|
||||
|
||||
$gccVersion = (& gcc --version | Select-Object -First 1)
|
||||
$windresVersion = (& windres --version | Select-Object -First 1)
|
||||
Write-Info "Using compiler: $gccVersion"
|
||||
Write-Info "Using resource compiler: $windresVersion"
|
||||
|
||||
$env:CGO_ENABLED = "1"
|
||||
$env:GOOS = "windows"
|
||||
$env:GOARCH = "amd64"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path "dist" | Out-Null
|
||||
|
||||
$internalWintun = "internal\wintunloader\assets\wintun\windows\amd64\wintun.dll"
|
||||
$toolWintun = "tools\wintun\amd64\wintun.dll"
|
||||
if (Test-Path $internalWintun) {
|
||||
Write-Info "Embedding internal Wintun DLL from $internalWintun"
|
||||
} elseif (Test-Path $toolWintun) {
|
||||
Write-Info "No embedded Wintun asset found; copying $toolWintun beside the EXE as runtime fallback"
|
||||
Copy-Item -Force $toolWintun "dist\wintun.dll"
|
||||
} else {
|
||||
Write-Info "No Wintun DLL found in project. TUN mode will ask for wintun.dll unless you embed it before building."
|
||||
}
|
||||
|
||||
|
||||
$manifest = "cmd\socksrevivepc\socksrevivepc.exe.manifest"
|
||||
$resource = "cmd\socksrevivepc\socksrevivepc_windows.rc"
|
||||
$syso = "cmd\socksrevivepc\admin_windows.syso"
|
||||
if (-not (Test-Path $manifest)) {
|
||||
throw "Missing Windows UAC manifest: $manifest"
|
||||
}
|
||||
if (-not (Test-Path $resource)) {
|
||||
throw "Missing Windows resource script: $resource"
|
||||
}
|
||||
Write-Info "Embedding UAC manifest: requireAdministrator"
|
||||
Run-Native "windres" @("-O", "coff", "-F", "pe-x86-64", "-i", $resource, "-o", $syso)
|
||||
if (-not (Test-Path $syso)) {
|
||||
throw "windres finished but $syso was not created."
|
||||
}
|
||||
|
||||
if (-not $NoTidy) {
|
||||
Run-Native "go" @("mod", "tidy")
|
||||
}
|
||||
|
||||
Run-Native "go" @("build", "-trimpath", "-ldflags", "-s -w -H=windowsgui", "-o", "dist/SocksRevivePC.exe", "./cmd/socksrevivepc")
|
||||
|
||||
if (-not (Test-Path "dist/SocksRevivePC.exe")) {
|
||||
throw "Build finished but dist/SocksRevivePC.exe was not created."
|
||||
}
|
||||
|
||||
Write-Info "Built dist/SocksRevivePC.exe"
|
||||
Write-Info "Normal build is GUI-only: no console window is attached."
|
||||
|
||||
if ($Debug) {
|
||||
Run-Native "go" @("build", "-trimpath", "-o", "dist/SocksRevivePC_debug.exe", "./cmd/socksrevivepc")
|
||||
if (-not (Test-Path "dist/SocksRevivePC_debug.exe")) {
|
||||
throw "Build finished but dist/SocksRevivePC_debug.exe was not created."
|
||||
}
|
||||
Write-Info "Built dist/SocksRevivePC_debug.exe for crash/debug logs. This debug EXE may show a console by design."
|
||||
} else {
|
||||
if (Test-Path "dist/SocksRevivePC_debug.exe") {
|
||||
Remove-Item -Force "dist/SocksRevivePC_debug.exe"
|
||||
}
|
||||
Write-Info "Skipped debug console EXE. Use -Debug only when troubleshooting."
|
||||
}
|
||||
Write-Info "Windows UAC: SocksRevivePC.exe is marked requireAdministrator and should show the admin prompt before starting."
|
||||
if (Test-Path $internalWintun) {
|
||||
Write-Info "Windows TUN: Wintun is embedded into the EXE and will be extracted automatically at runtime."
|
||||
} elseif (Test-Path "dist\wintun.dll") {
|
||||
Write-Info "Windows TUN: copied wintun.dll beside the EXE."
|
||||
} else {
|
||||
Write-Info "Windows TUN: run as Administrator and provide official wintun.dll, or embed it and rebuild."
|
||||
}
|
||||
24
scripts/diagnose_windows_tun.ps1
Normal file
24
scripts/diagnose_windows_tun.ps1
Normal file
@@ -0,0 +1,24 @@
|
||||
$ErrorActionPreference = 'Continue'
|
||||
Write-Host '[SocksRevivePC] Windows TUN diagnostics'
|
||||
Write-Host ''
|
||||
Write-Host '== Wintun / tunnel adapters =='
|
||||
Get-NetAdapter | Where-Object { $_.Name -like '*wintun*' -or $_.InterfaceDescription -like '*Wintun*' -or $_.InterfaceDescription -like '*WireGuard*' } | Format-List Name,InterfaceDescription,InterfaceIndex,Status,MacAddress,LinkSpeed
|
||||
Write-Host ''
|
||||
Write-Host '== Adapter IP addresses =='
|
||||
Get-NetIPAddress | Where-Object { $_.InterfaceAlias -like '*wintun*' -or $_.InterfaceAlias -like '*WireGuard*' } | Format-Table InterfaceAlias,InterfaceIndex,AddressFamily,IPAddress,PrefixLength -AutoSize
|
||||
Write-Host ''
|
||||
Write-Host '== Split default routes =='
|
||||
Get-NetRoute -DestinationPrefix '0.0.0.0/1','128.0.0.0/1','::/1','8000::/1' -ErrorAction SilentlyContinue | Sort-Object AddressFamily,DestinationPrefix,RouteMetric | Format-Table DestinationPrefix,InterfaceAlias,InterfaceIndex,NextHop,RouteMetric,AddressFamily -AutoSize
|
||||
Write-Host ''
|
||||
Write-Host '== Normal default routes =='
|
||||
Get-NetRoute -DestinationPrefix '0.0.0.0/0','::/0' -ErrorAction SilentlyContinue | Sort-Object AddressFamily,RouteMetric,InterfaceMetric | Format-Table DestinationPrefix,InterfaceAlias,InterfaceIndex,NextHop,RouteMetric,InterfaceMetric,AddressFamily -AutoSize
|
||||
Write-Host ''
|
||||
Write-Host '== DNS servers =='
|
||||
Get-DnsClientServerAddress | Where-Object { $_.InterfaceAlias -like '*wintun*' -or $_.InterfaceAlias -like '*WireGuard*' -or $_.AddressFamily -eq 2 } | Format-Table InterfaceAlias,AddressFamily,ServerAddresses -AutoSize
|
||||
Write-Host ''
|
||||
Write-Host 'If 0.0.0.0/1 and 128.0.0.0/1 do not point to the Wintun adapter, the app is not running as Administrator or Windows rejected the route.'
|
||||
Write-Host ''
|
||||
Write-Host '== Quick IPv6 test =='
|
||||
try { Test-NetConnection -ComputerName '2606:4700:4700::1111' -Port 443 -InformationLevel Detailed } catch { Write-Host $_ }
|
||||
Write-Host ''
|
||||
Write-Host 'IPv6 note: if IPv6 support is OFF but leak protection is ON, ::/1 and 8000::/1 should point to Wintun and public IPv6 tests should fail instead of showing your ISP IPv6.'
|
||||
31
scripts/embed_wintun_from_tools.ps1
Normal file
31
scripts/embed_wintun_from_tools.ps1
Normal file
@@ -0,0 +1,31 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$source = "tools\wintun\amd64\wintun.dll"
|
||||
$targetDir = "internal\wintunloader\assets\wintun\windows\amd64"
|
||||
$target = Join-Path $targetDir "wintun.dll"
|
||||
|
||||
function Test-PEAmd64($Path) {
|
||||
$bytes = [System.IO.File]::ReadAllBytes($Path)
|
||||
if ($bytes.Length -lt 1024) { throw "Invalid wintun.dll: file is too small." }
|
||||
if ($bytes[0] -ne 0x4d -or $bytes[1] -ne 0x5a) { throw "Invalid wintun.dll: missing MZ header." }
|
||||
$peOffset = [BitConverter]::ToInt32($bytes, 0x3c)
|
||||
if ($peOffset -le 0 -or $bytes.Length -lt ($peOffset + 6)) { throw "Invalid wintun.dll: bad PE header." }
|
||||
if ($bytes[$peOffset] -ne 0x50 -or $bytes[$peOffset+1] -ne 0x45 -or $bytes[$peOffset+2] -ne 0x00 -or $bytes[$peOffset+3] -ne 0x00) {
|
||||
throw "Invalid wintun.dll: missing PE signature."
|
||||
}
|
||||
$machine = [BitConverter]::ToUInt16($bytes, $peOffset + 4)
|
||||
if ($machine -ne 0x8664) {
|
||||
$hex = "0x{0:x4}" -f $machine
|
||||
throw "Wrong wintun.dll architecture: got $hex, expected amd64/x64 machine 0x8664. Use wintun\bin\amd64\wintun.dll from the official Wintun ZIP."
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-Path $source)) {
|
||||
throw "Missing $source. Put the official signed amd64 wintun.dll there first."
|
||||
}
|
||||
|
||||
Test-PEAmd64 $source
|
||||
New-Item -ItemType Directory -Force -Path $targetDir | Out-Null
|
||||
Copy-Item -Force $source $target
|
||||
Write-Host "Embedded Wintun asset prepared: $target"
|
||||
Write-Host "Now rebuild with: powershell -ExecutionPolicy Bypass -File .\scripts\build_windows.ps1"
|
||||
41
scripts/install_windows_compiler_msys2.ps1
Normal file
41
scripts/install_windows_compiler_msys2.ps1
Normal file
@@ -0,0 +1,41 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Write-Info($Message) {
|
||||
Write-Host "[SocksRevivePC] $Message"
|
||||
}
|
||||
|
||||
$msysRoot = "C:\msys64"
|
||||
$bash = Join-Path $msysRoot "usr\bin\bash.exe"
|
||||
|
||||
if (-not (Test-Path $bash)) {
|
||||
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
|
||||
throw "winget was not found. Install MSYS2 manually from https://www.msys2.org, then run this script again."
|
||||
}
|
||||
|
||||
Write-Info "Installing MSYS2 with winget..."
|
||||
winget install --id MSYS2.MSYS2 -e --accept-source-agreements --accept-package-agreements
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "winget failed to install MSYS2. Install MSYS2 manually from https://www.msys2.org."
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-Path $bash)) {
|
||||
throw "MSYS2 was not found at $msysRoot after installation. Install MSYS2 manually or adjust this script path."
|
||||
}
|
||||
|
||||
Write-Info "Installing UCRT64 GCC and resource compiler toolchain..."
|
||||
& $bash -lc "pacman -Sy --needed --noconfirm mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-binutils"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "pacman failed to install mingw-w64-ucrt-x86_64-gcc/binutils. Open 'MSYS2 UCRT64' and run: pacman -S --needed mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-binutils"
|
||||
}
|
||||
|
||||
$ucrtPath = "C:\msys64\ucrt64\bin"
|
||||
$currentUserPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
||||
if (($currentUserPath -split ';') -notcontains $ucrtPath) {
|
||||
[Environment]::SetEnvironmentVariable("Path", "$ucrtPath;$currentUserPath", "User")
|
||||
Write-Info "Added $ucrtPath to your user PATH. Open a new PowerShell window before building."
|
||||
} else {
|
||||
Write-Info "$ucrtPath is already in your user PATH."
|
||||
}
|
||||
|
||||
Write-Info "Done. Open a new PowerShell window and run: powershell -ExecutionPolicy Bypass -File .\scripts\build_windows.ps1"
|
||||
29
scripts/run_debug_windows.ps1
Normal file
29
scripts/run_debug_windows.ps1
Normal file
@@ -0,0 +1,29 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Test-IsAdmin {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
|
||||
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
$root = (Get-Location).Path
|
||||
$scriptPath = $MyInvocation.MyCommand.Path
|
||||
|
||||
if (-not (Test-IsAdmin)) {
|
||||
Write-Host "SocksRevivePC debug runner needs Administrator permission for TUN/routes. Showing UAC prompt..."
|
||||
$args = "-NoExit -ExecutionPolicy Bypass -File `"$scriptPath`""
|
||||
Start-Process -FilePath "powershell.exe" -ArgumentList $args -WorkingDirectory $root -Verb RunAs -Wait
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
$exe = Join-Path $root "dist\SocksRevivePC_debug.exe"
|
||||
if (-not (Test-Path $exe)) {
|
||||
throw "dist\SocksRevivePC_debug.exe not found. Build it only when troubleshooting with: powershell -ExecutionPolicy Bypass -File .\scripts\build_windows.ps1 -Debug"
|
||||
}
|
||||
Write-Host "Starting debug build as Administrator. If the GUI closes, check logs\crash.log and logs\runtime.log."
|
||||
& $exe
|
||||
Write-Host "Process exited with code $LASTEXITCODE"
|
||||
if (Test-Path "logs\crash.log") {
|
||||
Write-Host "Last crash log lines:"
|
||||
Get-Content "logs\crash.log" -Tail 80
|
||||
}
|
||||
13
tools/dnstt/README.md
Normal file
13
tools/dnstt/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# DNSTT folder
|
||||
|
||||
DNSTT is embedded in SocksRevive PC now. You normally do **not** need to put `dnstt-client.exe` or `dnstt-client` here.
|
||||
|
||||
This folder is kept only as a legacy fallback if you disable the embedded DNSTT engine in the DNSTT tab and intentionally run an external DNSTT executable.
|
||||
|
||||
For normal use, leave this enabled in the app:
|
||||
|
||||
```text
|
||||
Use embedded DNSTT engine / no external EXE
|
||||
```
|
||||
|
||||
Then fill resolver type/address, tunnel domain, server public key, and local SSH host/port directly in the UI.
|
||||
44
tools/wintun/README.md
Normal file
44
tools/wintun/README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Wintun for Windows TUN mode
|
||||
|
||||
The Go TUN/tun2socks logic is compiled into SocksRevive PC. Windows still needs the official signed `wintun.dll` driver loader because Windows does not provide a pure userspace TUN interface.
|
||||
|
||||
You have two options.
|
||||
|
||||
## Option A: self-contained EXE / internal Wintun
|
||||
|
||||
1. Put the official amd64 `wintun.dll` here:
|
||||
|
||||
```text
|
||||
tools/wintun/amd64/wintun.dll
|
||||
```
|
||||
|
||||
2. Run:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\embed_wintun_from_tools.ps1
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\build_windows.ps1
|
||||
```
|
||||
|
||||
The script copies the DLL into:
|
||||
|
||||
```text
|
||||
internal/wintunloader/assets/wintun/windows/amd64/wintun.dll
|
||||
```
|
||||
|
||||
Then Go embeds it into `SocksRevivePC.exe`. At runtime the app extracts it automatically before creating the TUN adapter.
|
||||
|
||||
## Option B: external DLL fallback
|
||||
|
||||
Put `wintun.dll` beside `SocksRevivePC.exe` or in:
|
||||
|
||||
```text
|
||||
tools/wintun/amd64/wintun.dll
|
||||
```
|
||||
|
||||
The app will copy/detect it at runtime.
|
||||
|
||||
## Notes
|
||||
|
||||
- Run the app as Administrator for TUN mode.
|
||||
- Use only the signed DLL from the official Wintun package.
|
||||
- The default adapter/device is `wintun`.
|
||||
4
tools/wintun/amd64/PUT_WINTUN_DLL_HERE.txt
Normal file
4
tools/wintun/amd64/PUT_WINTUN_DLL_HERE.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
Put official signed amd64 wintun.dll in this folder if you want either:
|
||||
|
||||
1. Runtime fallback: build script copies it beside dist/SocksRevivePC.exe.
|
||||
2. Internal/self-contained EXE: run scripts/embed_wintun_from_tools.ps1 before build.
|
||||
BIN
tools/wintun/amd64/wintun.dll
Normal file
BIN
tools/wintun/amd64/wintun.dll
Normal file
Binary file not shown.
1
tools/wintun/arm64/PUT_WINTUN_DLL_HERE.txt
Normal file
1
tools/wintun/arm64/PUT_WINTUN_DLL_HERE.txt
Normal file
@@ -0,0 +1 @@
|
||||
Put official signed arm64 wintun.dll in this folder if building Windows arm64.
|
||||
BIN
tools/wintun/arm64/wintun.dll
Normal file
BIN
tools/wintun/arm64/wintun.dll
Normal file
Binary file not shown.
14
tools/xray/README.md
Normal file
14
tools/xray/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Xray executable folder
|
||||
|
||||
Put your Xray executable here:
|
||||
|
||||
- Windows: `tools/xray/xray.exe`
|
||||
- Linux/macOS: `tools/xray/xray`
|
||||
|
||||
The default Xray profile starts:
|
||||
|
||||
```text
|
||||
xray run -config configs/xray.json
|
||||
```
|
||||
|
||||
Edit `configs/xray.json` with your real VLESS/VMess/Trojan/Shadowsocks settings. The app expects Xray to expose a SOCKS inbound at `127.0.0.1:10808`, unless you change that in the Xray tab.
|
||||
Reference in New Issue
Block a user