mirror of
https://github.com/hackdiaz-dev/free-turn-proxy.git
synced 2026-10-12 09:41:53 +02:00
fix: устранить замечания комплексной проверки
Критичное: - go.mod: версия 1.25.5→1.25.0 (go mod tidy); Dockerfile: тег 1.25 - release.yml: go-version 1.25.5 → stable - удалён temp-файл types.go.tmp.18500.1c9f4278a15a - udprelay/loop.go: defer conn1/conn2.Close() при сбое Dial; лог write-error Безопасность/код: - browserprofile: пермиссии 0644→0600 - bondserver: once.Do перед go readLane (ordering fix) - cmd/server: err= → err:= (shadow fix в AfterFunc) - vkauth/client: vkDelayRandom ctx-aware (возвращает error) - captcha/manual: whitelist хостов в /generic_proxy (SSRF mitigation) Инфра: - .dockerignore: исключить .git/.github/notes/dist из build context - docker-compose.yml: env_file .env для OBF_KEY - entrypoint.sh: позиционные аргументы вместо unquoted vars; валидация host:port CI: - ci.yml: исправлен путь go module cache (~/go/pkg/mod); добавлен -race - .gitignore: добавлены /dist/, *.test, coverage.out, .env, *.tmp - .golangci.yml: добавлены gosec и staticcheck - routes-macos.sh: set -eu вместо set -u Код: - bondclient/bondserver: errors.Is(err, io.EOF) вместо err != io.EOF - cmd/client: константа dtlsHandshakeConcurrency для HandshakeSem Доки: - quickstart.md: добавлен Intel Mac (darwin-amd64) - build.md: версия Go, Windows/macOS cross-compile инструкции - flags.md: уточнено описание -bond
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
.git
|
||||
.github
|
||||
.idea
|
||||
.claude
|
||||
notes
|
||||
scripts
|
||||
docs
|
||||
*.md
|
||||
dist
|
||||
coverage.out
|
||||
*.test
|
||||
@@ -27,8 +27,7 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
# Module cache
|
||||
${{ github.workspace }}/pkg/mod
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
@@ -54,7 +53,7 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
go test ./... -v
|
||||
go test -race ./... -v
|
||||
|
||||
build:
|
||||
name: Build binaries
|
||||
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.5"
|
||||
go-version: "stable"
|
||||
|
||||
# Install Android NDK only for android rows
|
||||
- name: Setup Android NDK
|
||||
|
||||
@@ -2,4 +2,10 @@
|
||||
.claude
|
||||
*.exe
|
||||
*.so
|
||||
*.tmp
|
||||
*.tmp.*
|
||||
/dist/
|
||||
/build/
|
||||
*.test
|
||||
coverage.out
|
||||
.env
|
||||
|
||||
@@ -13,6 +13,8 @@ linters:
|
||||
- unparam
|
||||
- gocritic
|
||||
- prealloc
|
||||
- gosec
|
||||
- staticcheck
|
||||
settings:
|
||||
errcheck:
|
||||
check-type-assertions: true
|
||||
|
||||
+4
-2
@@ -25,6 +25,8 @@ import (
|
||||
"github.com/samosvalishe/btp/internal/wire/srtpmimicry"
|
||||
)
|
||||
|
||||
const dtlsHandshakeConcurrency = 3
|
||||
|
||||
// manualCaptchaSolver связывает контракт vkauth.ManualSolveFunc
|
||||
// с локальным captcha-обработчиком (internal/client/captcha/manual).
|
||||
func manualCaptchaSolver(ctx context.Context, e *captcha.Error, d net.Dialer) (string, string, error) {
|
||||
@@ -107,7 +109,7 @@ func main() {
|
||||
if cfg.Proxy.Mode != config.ProxyModeUDP {
|
||||
tcpDtlsDialer := &dtlsdial.Dialer{
|
||||
HandshakeTimeout: 30 * time.Second,
|
||||
HandshakeSem: make(chan struct{}, 3),
|
||||
HandshakeSem: make(chan struct{}, dtlsHandshakeConcurrency),
|
||||
}
|
||||
bondH := &bondclient.Handler{Deps: bondclient.Deps{Log: logger}}
|
||||
tcpDeps := &tcpfwd.Deps{
|
||||
@@ -134,7 +136,7 @@ func main() {
|
||||
|
||||
udpDtlsDialer := &dtlsdial.Dialer{
|
||||
HandshakeTimeout: 20 * time.Second,
|
||||
HandshakeSem: make(chan struct{}, 3),
|
||||
HandshakeSem: make(chan struct{}, dtlsHandshakeConcurrency),
|
||||
}
|
||||
udpParams := &udprelay.Params{
|
||||
Host: cfg.TURN.Host,
|
||||
|
||||
+1
-1
@@ -96,7 +96,7 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
context.AfterFunc(ctx, func() {
|
||||
if err = listener.Close(); err != nil {
|
||||
if err := listener.Close(); err != nil {
|
||||
logger.Errorf("listener close: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
+6
-1
@@ -4,11 +4,16 @@ services:
|
||||
# build: .
|
||||
network_mode: host
|
||||
restart: unless-stopped
|
||||
# Секретные переменные (OBF_KEY) храните в .env рядом с этим файлом,
|
||||
# а не здесь. Пример: echo "OBF_KEY=<64hex>" >> .env
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
environment:
|
||||
# Backend address: WireGuard → 127.0.0.1:51820 | Xray/sing-box → 127.0.0.1:443
|
||||
CONNECT_ADDR: "127.0.0.1:51820"
|
||||
# LISTEN_ADDR: "0.0.0.0:56000"
|
||||
# MODE: "tcp" # tcp = TCP-форвардер для Xray/sing-box; default udp = WireGuard
|
||||
# OBF_MODE: "true"
|
||||
# OBF_KEY: "" # 64 hex chars; required when OBF_MODE=true
|
||||
# OBF_KEY задаётся в .env (не коммитьте ключ в git)
|
||||
# DEBUG: "false"
|
||||
|
||||
+11
-9
@@ -4,22 +4,24 @@ set -e
|
||||
CONNECT="${CONNECT_ADDR:?CONNECT_ADDR is required}"
|
||||
LISTEN="${LISTEN_ADDR:-0.0.0.0:56000}"
|
||||
|
||||
MODE_FLAG=""
|
||||
case "$CONNECT" in
|
||||
*:*) ;;
|
||||
*) echo "CONNECT_ADDR must be in host:port format" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
set -- -listen "$LISTEN" -connect "$CONNECT"
|
||||
|
||||
if [ "${MODE}" = "tcp" ]; then
|
||||
MODE_FLAG="-mode tcp"
|
||||
set -- "$@" -mode tcp
|
||||
fi
|
||||
|
||||
OBF_FLAG=""
|
||||
OBF_KEY_FLAG=""
|
||||
if [ "${OBF_MODE}" = "true" ]; then
|
||||
OBF="${OBF_KEY:?OBF_KEY is required when OBF_MODE=true}"
|
||||
OBF_FLAG="-obf"
|
||||
OBF_KEY_FLAG="-obf-key $OBF"
|
||||
set -- "$@" -obf -obf-key "$OBF"
|
||||
fi
|
||||
|
||||
DEBUG_FLAG=""
|
||||
if [ "${DEBUG}" = "true" ]; then
|
||||
DEBUG_FLAG="-debug"
|
||||
set -- "$@" -debug
|
||||
fi
|
||||
|
||||
exec ./server -listen "$LISTEN" -connect "$CONNECT" $MODE_FLAG $OBF_FLAG $OBF_KEY_FLAG $DEBUG_FLAG
|
||||
exec ./server "$@"
|
||||
|
||||
+31
-1
@@ -1,6 +1,6 @@
|
||||
# Сборка
|
||||
|
||||
Нужен Go 1.25.x.
|
||||
Нужен Go 1.24.x.
|
||||
|
||||
```bash
|
||||
go build -o client ./cmd/client
|
||||
@@ -44,3 +44,33 @@ GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w -checklinkname=0"
|
||||
```
|
||||
|
||||
> Все `.so` выше — обычные ELF под `.so`-именем (`CGO_ENABLED=0`, не настоящий shared library). Настоящий `c-shared` — `CGO_ENABLED=1` + NDK toolchain (см. `.github/workflows/release.yml`).
|
||||
|
||||
**Windows amd64:**
|
||||
|
||||
```powershell
|
||||
$env:GOOS="windows"; $env:GOARCH="amd64"; $env:CGO_ENABLED="0"
|
||||
go build -ldflags="-s -w -checklinkname=0" -trimpath -o client-windows-amd64.exe ./cmd/client
|
||||
go build -ldflags="-s -w -checklinkname=0" -trimpath -o server-windows-amd64.exe ./cmd/server
|
||||
```
|
||||
|
||||
**Windows arm64:**
|
||||
|
||||
```powershell
|
||||
$env:GOOS="windows"; $env:GOARCH="arm64"; $env:CGO_ENABLED="0"
|
||||
go build -ldflags="-s -w -checklinkname=0" -trimpath -o client-windows-arm64.exe ./cmd/client
|
||||
go build -ldflags="-s -w -checklinkname=0" -trimpath -o server-windows-arm64.exe ./cmd/server
|
||||
```
|
||||
|
||||
**macOS amd64 (Intel):**
|
||||
|
||||
```bash
|
||||
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o client-darwin-amd64 ./cmd/client
|
||||
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o server-darwin-amd64 ./cmd/server
|
||||
```
|
||||
|
||||
**macOS arm64 (Apple Silicon):**
|
||||
|
||||
```bash
|
||||
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o client-darwin-arm64 ./cmd/client
|
||||
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o server-darwin-arm64 ./cmd/server
|
||||
```
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
| `-n` | `10` | параллельных TURN-потоков |
|
||||
| `-transport` | `tcp` | транспорт до TURN-реле: `tcp` (TCP/TLS) \| `udp` |
|
||||
| `-mode` | `udp` | режим туннеля: `udp` (UDP-релей для WireGuard) \| `tcp` (TCP-форвардер для Xray/sing-box) |
|
||||
| `-bond` | `false` | распределять TCP-соединение по smux-сессиям (только с `-mode tcp`) |
|
||||
| `-bond` | `false` | распределять одно TCP-соединение по всем активным smux-сессиям (только с `-mode tcp`) |
|
||||
| `-turn` | из ссылки | переопределить IP TURN-сервера |
|
||||
| `-port` | из ссылки | переопределить порт TURN-сервера |
|
||||
| `-obf` | `false` | SRTP-мимикрия payload |
|
||||
|
||||
+9
-1
@@ -41,7 +41,7 @@ Invoke-WebRequest -Uri https://github.com/samosvalishe/btp/releases/latest/downl
|
||||
.\client.exe -listen 127.0.0.1:9000 -peer <vps>:56000 -link "<vk-link>" -debug 2>&1 | .\scripts\routes.ps1
|
||||
```
|
||||
|
||||
**macOS:**
|
||||
**macOS (Apple Silicon / ARM):**
|
||||
|
||||
```bash
|
||||
curl -L -o client https://github.com/samosvalishe/btp/releases/latest/download/client-darwin-arm64
|
||||
@@ -49,6 +49,14 @@ chmod +x client
|
||||
./client -listen 127.0.0.1:9000 -peer <vps>:56000 -link "<vk-link>" -debug 2>&1 | ./scripts/routes-macos.sh
|
||||
```
|
||||
|
||||
**macOS (Intel / amd64):**
|
||||
|
||||
```bash
|
||||
curl -L -o client https://github.com/samosvalishe/btp/releases/latest/download/client-darwin-amd64
|
||||
chmod +x client
|
||||
./client -listen 127.0.0.1:9000 -peer <vps>:56000 -link "<vk-link>" -debug 2>&1 | ./scripts/routes-macos.sh
|
||||
```
|
||||
|
||||
Когда скрипт напечатает `Ensuring route to ...` — включайте WireGuard.
|
||||
|
||||
> Скачали только бинарник без репо? Возьмите скрипты отдельно:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/samosvalishe/btp
|
||||
|
||||
go 1.25.5
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/bogdanfinn/fhttp v0.6.8
|
||||
|
||||
@@ -39,7 +39,7 @@ func Save(sp Saved) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(profileFile, data, 0644)
|
||||
return os.WriteFile(profileFile, data, 0600)
|
||||
}
|
||||
|
||||
func ApplyFhttp(req *fhttp.Request, profile Profile) {
|
||||
|
||||
@@ -65,6 +65,20 @@ func localCaptchaHosts() []string {
|
||||
}
|
||||
}
|
||||
|
||||
func isAllowedProxyHost(hostname string) bool {
|
||||
allowed := []string{
|
||||
".vk.com", ".vk.ru", ".vkontakte.ru",
|
||||
".userapi.com", ".okcdn.ru", ".mycdn.me",
|
||||
".api.vk.ru",
|
||||
}
|
||||
for _, suffix := range allowed {
|
||||
if strings.HasSuffix(hostname, suffix) || hostname == suffix[1:] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isLocalCaptchaHost(host string) bool {
|
||||
for _, localHost := range localCaptchaHosts() {
|
||||
if strings.EqualFold(host, localHost) {
|
||||
@@ -790,6 +804,10 @@ func SolveViaProxy(ctx context.Context, redirectURI string, dialer net.Dialer) (
|
||||
http.Error(w, "Bad URL", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if !isAllowedProxyHost(targetParsed.Hostname()) {
|
||||
http.Error(w, "Forbidden host", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
genericReverse := &httputil.ReverseProxy{
|
||||
Transport: transport,
|
||||
Rewrite: func(req *httputil.ProxyRequest) {
|
||||
|
||||
@@ -237,7 +237,12 @@ func (c *Client) fetch(ctx context.Context, link string, streamID int) (string,
|
||||
return "", "", nil, fmt.Errorf("all VK credentials failed: %w", lastErr)
|
||||
}
|
||||
|
||||
func vkDelayRandom(minMs, maxMs int) {
|
||||
func vkDelayRandom(ctx context.Context, minMs, maxMs int) error {
|
||||
ms := minMs + rand.Intn(maxMs-minMs+1)
|
||||
time.Sleep(time.Duration(ms) * time.Millisecond)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(time.Duration(ms) * time.Millisecond):
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,9 @@ func (c *Client) getTokenChain(ctx context.Context, link string, streamID int, c
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
vkDelayRandom(100, 150)
|
||||
if err := vkDelayRandom(ctx, 100, 150); err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
// Шаг 1a: прогрев getCallPreview (не критично).
|
||||
previewData := fmt.Sprintf("vk_join_link=https://vk.com/call/join/%s&fields=photo_200&access_token=%s", link, token1)
|
||||
@@ -53,7 +55,9 @@ func (c *Client) getTokenChain(ctx context.Context, link string, streamID int, c
|
||||
c.log.Warnf("[STREAM %d] [VK Auth] getCallPreview failed: %v", streamID, prevErr)
|
||||
}
|
||||
|
||||
vkDelayRandom(200, 400)
|
||||
if err := vkDelayRandom(ctx, 200, 400); err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
// Шаг 2: анонимный call-токен (здесь может сработать captcha).
|
||||
token2, err := c.fetchCallToken(ctx, httpClient, profile, streamID, link, escapedName, token1, creds)
|
||||
@@ -61,7 +65,9 @@ func (c *Client) getTokenChain(ctx context.Context, link string, streamID int, c
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
vkDelayRandom(100, 150)
|
||||
if err := vkDelayRandom(ctx, 100, 150); err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
// Шаг 3: ok.ru session_key.
|
||||
sessionKey, err := c.fetchOkRuSession(ctx, httpClient, profile)
|
||||
@@ -69,7 +75,9 @@ func (c *Client) getTokenChain(ctx context.Context, link string, streamID int, c
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
vkDelayRandom(100, 150)
|
||||
if err := vkDelayRandom(ctx, 100, 150); err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
// Шаг 4: TURN-реквизиты.
|
||||
return c.fetchTurnCreds(ctx, httpClient, profile, streamID, link, token2, sessionKey)
|
||||
|
||||
@@ -6,6 +6,7 @@ package bondclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -129,7 +130,7 @@ func (h *Handler) Handle(ctx context.Context, tcpConn net.Conn, connID uint64, c
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
default:
|
||||
if err != io.EOF {
|
||||
if !errors.Is(err, io.EOF) {
|
||||
h.Deps.log().Debugf("[bond %d] session %d read frame error: %v", connID, l.ps.ID, err)
|
||||
}
|
||||
}
|
||||
@@ -178,7 +179,7 @@ func (h *Handler) copyTCPToBond(ctx context.Context, connID uint64, tcpConn net.
|
||||
seq++
|
||||
}
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
if !errors.Is(err, io.EOF) {
|
||||
h.Deps.log().Debugf("[bond %d] local TCP read finished with error: %v", connID, err)
|
||||
}
|
||||
for _, l := range lanes {
|
||||
|
||||
@@ -151,10 +151,10 @@ func (c *serverConn) addLane(l *serverLane, laneCount uint16) {
|
||||
default:
|
||||
}
|
||||
|
||||
go c.readLane(l)
|
||||
c.once.Do(func() {
|
||||
go c.run()
|
||||
})
|
||||
go c.readLane(l)
|
||||
}
|
||||
|
||||
func (c *serverConn) snapshotLanes() []*serverLane {
|
||||
@@ -207,7 +207,7 @@ func (c *serverConn) readLane(l *serverLane) {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
default:
|
||||
if err != io.EOF {
|
||||
if !errors.Is(err, io.EOF) {
|
||||
c.deps.log().Debugf("[bond %d] lane %d read error: %v (lanes=%d)", c.id, l.index, err, left)
|
||||
}
|
||||
if left == 0 {
|
||||
|
||||
@@ -127,6 +127,8 @@ func oneDTLS(ctx context.Context, deps *Deps, peer *net.UDPAddr, listenConn net.
|
||||
defer dtlscancel()
|
||||
|
||||
conn1, conn2 := connutil.AsyncPacketPipe()
|
||||
defer conn1.Close()
|
||||
defer conn2.Close()
|
||||
// TURNLoop может перезапускать oneTURN несколько раз в рамках одного DTLS
|
||||
// соединения, каждый раз перечитывая conn2; публикуем до завершения DTLS.
|
||||
go func() {
|
||||
@@ -177,6 +179,7 @@ func oneDTLS(ctx context.Context, deps *Deps, peer *net.UDPAddr, listenConn net.
|
||||
_, werr := dtlsConn.Write(pkt.Data[:pkt.N])
|
||||
packetPool.Put(pkt)
|
||||
if werr != nil {
|
||||
deps.log().Debugf("[STREAM %d] DTLS write error: %v", streamID, werr)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/zsh
|
||||
# Usage: ./client -debug ... 2>&1 | ./scripts/routes-macos.sh
|
||||
set -u
|
||||
set -eu
|
||||
|
||||
default_info="$(route -n get default 2>/dev/null || true)"
|
||||
default_if="$(printf '%s\n' "$default_info" | awk '/interface:/{print $2; exit}')"
|
||||
|
||||
Reference in New Issue
Block a user