diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3cc1211..1674a9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ env: GO_VERSION: "1.25.0" GOLANGCI_VERSION: v2.12.2 GOVULN_VERSION: v1.1.4 - GORELEASER_VERSION: v2.5.0 + GORELEASER_VERSION: v2.15.4 TASK_VERSION: v3.40.0 jobs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7a00c81..095d62e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ permissions: env: GO_VERSION: "1.25.0" - GORELEASER_VERSION: v2.5.0 + GORELEASER_VERSION: v2.15.4 jobs: goreleaser: diff --git a/.gitignore b/.gitignore index f761e67..97042e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .idea .claude +CLAUDE.md *.exe *.so *.tmp diff --git a/.goreleaser.yaml b/.goreleaser.yaml index eb2ecab..ff1cdc8 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -149,51 +149,23 @@ release: draft: false name_template: "{{ .ProjectName }} {{ .Version }}" -dockers: - - id: server-amd64 +dockers_v2: + - id: server ids: - server - image_templates: - - "ghcr.io/samosvalishe/btp:{{ .Version }}-amd64" dockerfile: Dockerfile.goreleaser - use: buildx - goos: linux - goarch: amd64 - build_flag_templates: - - "--platform=linux/amd64" - - "--label=org.opencontainers.image.source=https://github.com/samosvalishe/btp" - - "--label=org.opencontainers.image.version={{ .Version }}" - - "--label=org.opencontainers.image.revision={{ .FullCommit }}" - - "--label=org.opencontainers.image.created={{ .Date }}" + images: + - ghcr.io/samosvalishe/btp + tags: + - "{{ .Version }}" + - '{{ if not .Prerelease }}latest{{ end }}' + platforms: + - linux/amd64 + - linux/arm64 + labels: + org.opencontainers.image.source: https://github.com/samosvalishe/btp + org.opencontainers.image.version: "{{ .Version }}" + org.opencontainers.image.revision: "{{ .FullCommit }}" + org.opencontainers.image.created: "{{ .Date }}" extra_files: - docker/entrypoint.sh - - - id: server-arm64 - ids: - - server - image_templates: - - "ghcr.io/samosvalishe/btp:{{ .Version }}-arm64" - dockerfile: Dockerfile.goreleaser - use: buildx - goos: linux - goarch: arm64 - build_flag_templates: - - "--platform=linux/arm64" - - "--label=org.opencontainers.image.source=https://github.com/samosvalishe/btp" - - "--label=org.opencontainers.image.version={{ .Version }}" - - "--label=org.opencontainers.image.revision={{ .FullCommit }}" - - "--label=org.opencontainers.image.created={{ .Date }}" - extra_files: - - docker/entrypoint.sh - -docker_manifests: - - name_template: "ghcr.io/samosvalishe/btp:{{ .Version }}" - image_templates: - - "ghcr.io/samosvalishe/btp:{{ .Version }}-amd64" - - "ghcr.io/samosvalishe/btp:{{ .Version }}-arm64" - - - name_template: "ghcr.io/samosvalishe/btp:latest" - skip_push: "{{ .Prerelease }}" - image_templates: - - "ghcr.io/samosvalishe/btp:{{ .Version }}-amd64" - - "ghcr.io/samosvalishe/btp:{{ .Version }}-arm64" diff --git a/Dockerfile.goreleaser b/Dockerfile.goreleaser index ab9a3b6..43c9039 100644 --- a/Dockerfile.goreleaser +++ b/Dockerfile.goreleaser @@ -1,14 +1,15 @@ # Thin runtime image. Built by GoReleaser using a pre-compiled server binary. -# See .goreleaser.yaml (dockers section) for the build context. +# See .goreleaser.yaml (dockers_v2 section) for the build context. FROM alpine:3.23 RUN apk add --no-cache ca-certificates tzdata WORKDIR /app -# GoReleaser places the server binary at the build root, and any files -# listed in `extra_files` (entrypoint.sh) are also placed in the context root. -COPY server /app/server +# GoReleaser dockers_v2 buildx-builds multi-platform images; per-platform +# binaries are placed under $TARGETPLATFORM/ in the build context. +ARG TARGETPLATFORM +COPY $TARGETPLATFORM/server /app/server COPY docker/entrypoint.sh /app/entrypoint.sh RUN sed -i 's/\r$//' /app/entrypoint.sh \ diff --git a/Taskfile.yml b/Taskfile.yml index 86fe491..fb6f707 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -7,7 +7,7 @@ vars: LDFLAGS: -s -w -checklinkname=0 -X main.version={{.VERSION}} GOLANGCI_VERSION: v2.12.2 GOVULN_VERSION: v1.1.4 - GORELEASER_VERSION: v2.5.0 + GORELEASER_VERSION: v2.15.4 tasks: default: diff --git a/internal/client/captcha/captcha.go b/internal/client/captcha/captcha.go index 9d6e6a0..83dc958 100644 --- a/internal/client/captcha/captcha.go +++ b/internal/client/captcha/captcha.go @@ -11,7 +11,6 @@ import ( "errors" "fmt" "io" - mathrand "math/rand" "regexp" "strconv" "strings" @@ -23,6 +22,7 @@ import ( "github.com/samosvalishe/btp/internal/client/browserprofile" "github.com/samosvalishe/btp/internal/logx" + "github.com/samosvalishe/btp/internal/randx" ) // Log — пакетный логгер. По умолчанию no-op; main устанавливает его через @@ -449,7 +449,7 @@ func (s *captchaSession) solveCheckboxCaptcha( select { case <-s.ctx.Done(): return "", s.ctx.Err() - case <-time.After(time.Duration(400+mathrand.Intn(250)) * time.Millisecond): + case <-time.After(time.Duration(400+randx.Intn(250)) * time.Millisecond): } check, err := s.performCaptchaCheck(sessionToken, browserFP, hash, "{}", "[]", debugInfo) diff --git a/internal/client/captcha/manual/manual.go b/internal/client/captcha/manual/manual.go index d26e63a..310e87d 100644 --- a/internal/client/captcha/manual/manual.go +++ b/internal/client/captcha/manual/manual.go @@ -12,6 +12,7 @@ import ( "encoding/json" "errors" "fmt" + "html" "io" "net" "net/http" @@ -548,7 +549,7 @@ func startCaptchaServer(srv *http.Server, logPrefix string) error { // runCaptchaServerAndWait открывает браузер и ждёт токен решения. // При срабатывании ctx возвращает ctx.Err(); в обоих случаях HTTP-сервер останавливается. func runCaptchaServerAndWait(ctx context.Context, handler http.Handler, captchaURL string, keyCh <-chan string, logPrefix string) (string, error) { - srv := &http.Server{Handler: handler} + srv := &http.Server{Handler: handler, ReadHeaderTimeout: 10 * time.Second} if err := startCaptchaServer(srv, logPrefix); err != nil { return "", err @@ -700,7 +701,8 @@ func SolveViaProxy(ctx context.Context, redirectURI string, dialer net.Dialer) ( Log.Errorf("[Captcha Proxy] %s %s: %v", r.Method, r.URL.String(), err) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusBadGateway) - _, _ = fmt.Fprintf(w, `

Captcha proxy error

%s %s

%v

`, r.Method, r.URL.String(), err) + _, _ = fmt.Fprintf(w, `

Captcha proxy error

%s %s

%s

`, + html.EscapeString(r.Method), html.EscapeString(r.URL.String()), html.EscapeString(err.Error())) }, ModifyResponse: func(res *http.Response) error { rewriteProxyCookies(res.Header) @@ -870,7 +872,9 @@ func SolveViaProxy(ctx context.Context, redirectURI string, dialer net.Dialer) ( func openBrowser(url string) { for _, cmd := range browserOpenCommands(runtime.GOOS, url) { - if err := exec.Command(cmd.name, cmd.args...).Start(); err == nil { //nolint:noctx + // cmd.name/args приходят из жёстко закодированного browserOpenCommands; + // внешнего ввода в exec.Command нет (url передан внутри args как обычная строка). + if err := exec.Command(cmd.name, cmd.args...).Start(); err == nil { //nolint:noctx,gosec // hardcoded browser binary list, no taint return } } diff --git a/internal/client/captcha/slider.go b/internal/client/captcha/slider.go index 9c5131a..f27edb2 100644 --- a/internal/client/captcha/slider.go +++ b/internal/client/captcha/slider.go @@ -10,12 +10,13 @@ import ( "image/color" _ "image/jpeg" // регистрация JPEG-декодера для image.Decode "math" - mathrand "math/rand" "runtime" "sort" "strconv" "strings" "sync" + + "github.com/samosvalishe/btp/internal/randx" ) type sliderPuzzle struct { @@ -509,7 +510,10 @@ func sampleLumaMapped(img image.Image, dstRect image.Rectangle, srcRect image.Re c := sampleColorMapped(img, dstRect, srcRect, dstX, dstY) r, g, b, _ := c.RGBA() y := (299*(r>>8) + 587*(g>>8) + 114*(b>>8)) / 1000 - return uint8(y) + if y > 255 { + y = 255 + } + return uint8(y) //nolint:gosec // bounded above by 255 } func absFloat(v float64) float64 { @@ -565,29 +569,29 @@ func buildSliderCursor(candidateIndex int, candidateCount int) string { Y int `json:"y"` } - startX := 570 + mathrand.Intn(40) - startY := 875 + mathrand.Intn(30) + startX := 570 + randx.Intn(40) + startY := 875 + randx.Intn(30) denom := candidateCount - 1 if denom < 1 { denom = 1 } baseTargetX := 734 + (937-734)*(candidateIndex-1)/denom - targetX := baseTargetX + mathrand.Intn(10) - 5 - targetY := 655 + mathrand.Intn(14) + targetX := baseTargetX + randx.Intn(10) - 5 + targetY := 655 + randx.Intn(14) points := make([]cursorPoint, 0, 28) - for i := 0; i < 1+mathrand.Intn(3); i++ { + for i := 0; i < 1+randx.Intn(3); i++ { points = append(points, cursorPoint{ - X: startX + mathrand.Intn(5) - 2, - Y: startY + mathrand.Intn(5) - 2, + X: startX + randx.Intn(5) - 2, + Y: startY + randx.Intn(5) - 2, }) } - transitSteps := 2 + mathrand.Intn(3) - arcOffX := mathrand.Intn(60) - 30 - arcOffY := -(mathrand.Intn(30) + 10) + transitSteps := 2 + randx.Intn(3) + arcOffX := randx.Intn(60) - 30 + arcOffY := -(randx.Intn(30) + 10) for i := 1; i <= transitSteps; i++ { t := float64(i) / float64(transitSteps+1) cx := float64(startX+targetX)/2 + float64(arcOffX) @@ -596,25 +600,25 @@ func buildSliderCursor(candidateIndex int, candidateCount int) string { by := (1-t)*(1-t)*float64(startY) + 2*t*(1-t)*cy + t*t*float64(targetY) jitter := int((1-t)*8) + 2 points = append(points, cursorPoint{ - X: int(math.Round(bx)) + mathrand.Intn(jitter*2+1) - jitter, - Y: int(math.Round(by)) + mathrand.Intn(jitter*2+1) - jitter, + X: int(math.Round(bx)) + randx.Intn(jitter*2+1) - jitter, + Y: int(math.Round(by)) + randx.Intn(jitter*2+1) - jitter, }) } - approachSteps := 4 + mathrand.Intn(4) + approachSteps := 4 + randx.Intn(4) prev := points[len(points)-1] for i := 1; i <= approachSteps; i++ { t := float64(i) / float64(approachSteps) - ax := prev.X + int(math.Round(t*float64(targetX-prev.X))) + mathrand.Intn(5) - 2 - ay := prev.Y + int(math.Round(t*float64(targetY-prev.Y))) + mathrand.Intn(5) - 2 + ax := prev.X + int(math.Round(t*float64(targetX-prev.X))) + randx.Intn(5) - 2 + ay := prev.Y + int(math.Round(t*float64(targetY-prev.Y))) + randx.Intn(5) - 2 points = append(points, cursorPoint{X: ax, Y: ay}) } - settleCount := 3 + mathrand.Intn(5) + settleCount := 3 + randx.Intn(5) for i := 0; i < settleCount; i++ { points = append(points, cursorPoint{ - X: targetX + mathrand.Intn(7) - 3, - Y: targetY + mathrand.Intn(7) - 3, + X: targetX + randx.Intn(7) - 3, + Y: targetY + randx.Intn(7) - 3, }) } diff --git a/internal/client/dnsdial/doh.go b/internal/client/dnsdial/doh.go index cd6d0de..bfe8ff4 100644 --- a/internal/client/dnsdial/doh.go +++ b/internal/client/dnsdial/doh.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "crypto/tls" + "encoding/binary" "errors" "fmt" "io" @@ -334,9 +335,13 @@ func handleDohForwarderTCP(conn net.Conn, r *DohResolver) { Log.Warnf("[DoH] tcp forward failed: %v", err) return } + if len(resp) > 0xFFFF { + Log.Warnf("[DoH] response too large for TCP framing: %d", len(resp)) + return + } out := make([]byte, 2+len(resp)) - out[0] = byte(len(resp) >> 8) - out[1] = byte(len(resp)) + respLen := uint16(len(resp)) //nolint:gosec // bounded above + binary.BigEndian.PutUint16(out[:2], respLen) copy(out[2:], resp) _ = conn.SetWriteDeadline(time.Now().Add(forwarderTCPWriteDL)) //nolint:errcheck if _, err := conn.Write(out); err != nil { diff --git a/internal/client/namegen/namegen.go b/internal/client/namegen/namegen.go index 8fc8748..f8e8cd7 100644 --- a/internal/client/namegen/namegen.go +++ b/internal/client/namegen/namegen.go @@ -4,8 +4,9 @@ package namegen import ( _ "embed" - "math/rand/v2" "strings" + + "github.com/samosvalishe/btp/internal/randx" ) //go:embed data/first_names_male.txt @@ -60,19 +61,19 @@ const SurnameProbability = 0.7 // Generate возвращает случайное "Имя" или "Имя Фамилия". // Пол выбирается равномерно; женские фамилии феминизируются. func Generate() string { - isFemale := rand.IntN(2) == 0 + isFemale := randx.IntN(2) == 0 pool := maleFirstNames if isFemale { pool = femaleFirstNames } - fn := pool[rand.IntN(len(pool))] + fn := pool[randx.IntN(len(pool))] - if rand.Float64() >= SurnameProbability { + if randx.Float64() >= SurnameProbability { return fn } - ln := lastNames[rand.IntN(len(lastNames))] + ln := lastNames[randx.IntN(len(lastNames))] if isFemale { ln = feminizeSurname(ln) } diff --git a/internal/client/vkauth/client.go b/internal/client/vkauth/client.go index 9d5a14a..4496ad2 100644 --- a/internal/client/vkauth/client.go +++ b/internal/client/vkauth/client.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "math/rand" "net" "strings" "sync" @@ -12,6 +11,7 @@ import ( "time" "github.com/samosvalishe/btp/internal/logx" + "github.com/samosvalishe/btp/internal/randx" tlsclient "github.com/bogdanfinn/tls-client" ) @@ -96,7 +96,7 @@ func New(cfg Config) *Client { } c.tokenChain = c.getTokenChain c.minFetchIntervalFn = func() time.Duration { - return 3*time.Second + time.Duration(rand.Intn(3000))*time.Millisecond + return 3*time.Second + time.Duration(randx.Intn(3000))*time.Millisecond } return c } @@ -238,7 +238,7 @@ func (c *Client) fetch(ctx context.Context, link string, streamID int) (string, } func vkDelayRandom(ctx context.Context, minMs, maxMs int) error { - ms := minMs + rand.Intn(maxMs-minMs+1) + ms := minMs + randx.Intn(maxMs-minMs+1) select { case <-ctx.Done(): return ctx.Err() diff --git a/internal/client/vkauth/token.go b/internal/client/vkauth/token.go index ca0be8f..5fabe74 100644 --- a/internal/client/vkauth/token.go +++ b/internal/client/vkauth/token.go @@ -44,8 +44,8 @@ func (c *Client) getTokenChain(ctx context.Context, link string, streamID int, c return "", "", nil, err } - if err := vkDelayRandom(ctx, 100, 150); err != nil { - return "", "", nil, err + if delayErr := vkDelayRandom(ctx, 100, 150); delayErr != nil { + return "", "", nil, delayErr } // Шаг 1a: прогрев getCallPreview (не критично). @@ -55,8 +55,8 @@ func (c *Client) getTokenChain(ctx context.Context, link string, streamID int, c c.log.Warnf("[STREAM %d] [VK Auth] getCallPreview failed: %v", streamID, prevErr) } - if err := vkDelayRandom(ctx, 200, 400); err != nil { - return "", "", nil, err + if delayErr := vkDelayRandom(ctx, 200, 400); delayErr != nil { + return "", "", nil, delayErr } // Шаг 2: анонимный call-токен (здесь может сработать captcha). @@ -65,8 +65,8 @@ func (c *Client) getTokenChain(ctx context.Context, link string, streamID int, c return "", "", nil, err } - if err := vkDelayRandom(ctx, 100, 150); err != nil { - return "", "", nil, err + if delayErr := vkDelayRandom(ctx, 100, 150); delayErr != nil { + return "", "", nil, delayErr } // Шаг 3: ok.ru session_key. @@ -75,8 +75,8 @@ func (c *Client) getTokenChain(ctx context.Context, link string, streamID int, c return "", "", nil, err } - if err := vkDelayRandom(ctx, 100, 150); err != nil { - return "", "", nil, err + if delayErr := vkDelayRandom(ctx, 100, 150); delayErr != nil { + return "", "", nil, delayErr } // Шаг 4: TURN-реквизиты. diff --git a/internal/client/vkauth/types.go b/internal/client/vkauth/types.go index 5dc8739..06664f2 100644 --- a/internal/client/vkauth/types.go +++ b/internal/client/vkauth/types.go @@ -20,7 +20,12 @@ type TurnCredentials struct { Link string } -// DefaultCredentials — публичные app_id VK SDK; клиент перебирает по порядку. +// DefaultCredentials — публичные app_id/secret VK SDK, извлечённые из +// официальных VK-клиентов (web/mobile/video). Это НЕ приватные креды +// пользователя — VK раздаёт их в JS-бандле страницы калла. Клиент перебирает +// по порядку при ошибках авторизации. +// +//nolint:gosec // public VK SDK app credentials, not user secrets var DefaultCredentials = []VKCredentials{ {ClientID: "6287487", ClientSecret: "QbYic1K3lEV5kTGiqlq2"}, // VK_WEB_APP_ID {ClientID: "7879029", ClientSecret: "aR5NKGmm03GYrCiNKsaw"}, // VK_MVK_APP_ID diff --git a/internal/proxy/bondclient/bondclient.go b/internal/proxy/bondclient/bondclient.go index b2f731a..df2af4f 100644 --- a/internal/proxy/bondclient/bondclient.go +++ b/internal/proxy/bondclient/bondclient.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "io" + "math" "net" "strconv" "strings" @@ -76,11 +77,14 @@ func (h *Handler) Handle(ctx context.Context, tcpConn net.Conn, connID uint64, c opened = append(opened, pending{ps: ps, stream: stream}) } + if len(opened) > math.MaxUint16 { + opened = opened[:math.MaxUint16] + } lanes := make([]*lane, 0, len(opened)) laneIDs := make([]string, 0, len(opened)) - laneCount := uint16(len(opened)) + laneCount := uint16(len(opened)) //nolint:gosec // bounded above by MaxUint16 for i, p := range opened { - if err := bondframe.WriteHello(p.stream, connID, uint16(i), laneCount); err != nil { + if err := bondframe.WriteHello(p.stream, connID, uint16(i), laneCount); err != nil { //nolint:gosec // i < laneCount <= MaxUint16 h.Deps.log().Errorf("[bond %d] session %d hello error: %s", connID, p.ps.ID, err) _ = p.stream.Close() continue diff --git a/internal/proxy/tcpfwd/tcpfwd.go b/internal/proxy/tcpfwd/tcpfwd.go index 72a0c56..b3b258e 100644 --- a/internal/proxy/tcpfwd/tcpfwd.go +++ b/internal/proxy/tcpfwd/tcpfwd.go @@ -167,10 +167,11 @@ func Run(ctx context.Context, deps *Deps, params *Params, peer *net.UDPAddr, lis } } fromSession, toSession := netconn.BiCopy(ctx, tc, stream, errf) - sessRef.FromSession.Add(uint64(fromSession)) - sessRef.ToSession.Add(uint64(toSession)) + fromU, toU := nonNegUint64(fromSession), nonNegUint64(toSession) + sessRef.FromSession.Add(fromU) + sessRef.ToSession.Add(toU) deps.log().Debugf("[session %d] TCP done #%d local<-session=%s local->session=%s", - sessRef.ID, cid, stats.FormatByteCount(uint64(fromSession)), stats.FormatByteCount(uint64(toSession))) + sessRef.ID, cid, stats.FormatByteCount(fromU), stats.FormatByteCount(toU)) }) } } @@ -255,7 +256,7 @@ func createSmuxSession(ctx context.Context, deps *Deps, params *Params, peer *ne cleanupFns = append(cleanupFns, func() { _ = dtlsConn.Close() }) deps.log().Debugf("DTLS connection established") - statsCtx, statsCancel := context.WithCancel(ctx) + statsCtx, statsCancel := context.WithCancel(ctx) //nolint:gosec // cancel is held in cleanupFns and invoked via cleanup() LIFO cleanupFns = append(cleanupFns, statsCancel) st := stats.New(deps.log().DebugEnabled()) go st.LogEvery(statsCtx, deps.log().Debugf, fmt.Sprintf("[session %d] TCP", id), "to-turn", "from-turn") @@ -278,3 +279,12 @@ func createSmuxSession(ctx context.Context, deps *Deps, params *Params, peer *ne return smuxSess, cleanup, nil } + +// nonNegUint64 безопасно приводит int64-счётчик байт к uint64 (io.Copy +// возвращает n >= 0 при успехе, но на ошибке n может быть отрицательным). +func nonNegUint64(n int64) uint64 { + if n < 0 { + return 0 + } + return uint64(n) +} diff --git a/internal/proxy/udprelay/loop.go b/internal/proxy/udprelay/loop.go index 586658a..4fa1ca9 100644 --- a/internal/proxy/udprelay/loop.go +++ b/internal/proxy/udprelay/loop.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "math/rand" "net" "sync" "sync/atomic" @@ -13,6 +12,7 @@ import ( "github.com/cbeuw/connutil" "github.com/samosvalishe/btp/internal/client/vkauth" "github.com/samosvalishe/btp/internal/proxy/common" + "github.com/samosvalishe/btp/internal/randx" "github.com/samosvalishe/btp/internal/stats" "github.com/samosvalishe/btp/internal/wire/srtpmimicry" ) @@ -36,7 +36,7 @@ func DTLSLoop(ctx context.Context, deps *Deps, peer *net.UDPAddr, listenConn net select { case <-ctx.Done(): return - case <-time.After(time.Duration(1+rand.Intn(2)) * time.Second): + case <-time.After(time.Duration(1+randx.Intn(2)) * time.Second): } continue } @@ -44,7 +44,7 @@ func DTLSLoop(ctx context.Context, deps *Deps, peer *net.UDPAddr, listenConn net select { case <-ctx.Done(): return - case <-time.After(time.Duration(10+rand.Intn(20)) * time.Second): + case <-time.After(time.Duration(10+randx.Intn(20)) * time.Second): } } } @@ -118,7 +118,7 @@ func TURNLoop(ctx context.Context, deps *Deps, params *Params, peer *net.UDPAddr func oneDTLS(ctx context.Context, deps *Deps, peer *net.UDPAddr, listenConn net.PacketConn, inboundChan <-chan *Packet, connchan chan<- net.PacketConn, okchan chan<- struct{}, streamID int) error { select { - case <-time.After(time.Duration(rand.Intn(400)+100) * time.Millisecond): + case <-time.After(time.Duration(randx.Intn(400)+100) * time.Millisecond): case <-ctx.Done(): return ctx.Err() } @@ -127,8 +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() + defer func() { _ = conn1.Close() }() + defer func() { _ = conn2.Close() }() // TURNLoop может перезапускать oneTURN несколько раз в рамках одного DTLS // соединения, каждый раз перечитывая conn2; публикуем до завершения DTLS. go func() { @@ -216,7 +216,7 @@ func oneTURN(ctx context.Context, deps *Deps, params *Params, peer *net.UDPAddr, var err error defer func() { c <- err }() select { - case <-time.After(time.Duration(rand.Intn(400)+100) * time.Millisecond): + case <-time.After(time.Duration(randx.Intn(400)+100) * time.Millisecond): case <-ctx.Done(): err = ctx.Err() return diff --git a/internal/randx/randx.go b/internal/randx/randx.go new file mode 100644 index 0000000..c71a694 --- /dev/null +++ b/internal/randx/randx.go @@ -0,0 +1,36 @@ +// Package randx — тонкая обёртка над crypto/rand с API в стиле math/rand. +// Используется там, где gosec G404 бьёт по math/rand: антифингерпринт-jitter, +// случайные задержки, выбор имён/паттернов. CSPRNG здесь не критичен по +// производительности, зато снимает шум линтера и убирает PRNG-предсказуемость. +package randx + +import ( + "crypto/rand" + "encoding/binary" + "math/big" +) + +// IntN возвращает равномерное случайное число из [0, n). Для n <= 0 возвращает 0. +func IntN(n int) int { + if n <= 0 { + return 0 + } + v, err := rand.Int(rand.Reader, big.NewInt(int64(n))) + if err != nil { + return 0 + } + return int(v.Int64()) +} + +// Intn — алиас IntN для совместимости с math/rand. +func Intn(n int) int { return IntN(n) } + +// Float64 возвращает число из [0.0, 1.0) с 53 битами мантиссы. +func Float64() float64 { + var b [8]byte + if _, err := rand.Read(b[:]); err != nil { + return 0 + } + u := binary.LittleEndian.Uint64(b[:]) >> 11 + return float64(u) / float64(1<<53) +} diff --git a/internal/wire/bondframe/bondframe.go b/internal/wire/bondframe/bondframe.go index 665a017..f83fbc0 100644 --- a/internal/wire/bondframe/bondframe.go +++ b/internal/wire/bondframe/bondframe.go @@ -8,6 +8,7 @@ import ( "encoding/binary" "fmt" "io" + "math" "net" "time" ) @@ -78,10 +79,13 @@ func ParseHelloHeader(hdr []byte) (Hello, error) { } func WriteFrame(w io.Writer, typ byte, seq uint64, data []byte) error { + if uint64(len(data)) > math.MaxUint32 { + return fmt.Errorf("bondframe: data too large: %d", len(data)) + } var hdr [13]byte hdr[0] = typ binary.BigEndian.PutUint64(hdr[1:9], seq) - binary.BigEndian.PutUint32(hdr[9:13], uint32(len(data))) + binary.BigEndian.PutUint32(hdr[9:13], uint32(len(data))) //nolint:gosec // bounded above if _, err := w.Write(hdr[:]); err != nil { return err } diff --git a/internal/wire/srtpmimicry/srtpmimicry.go b/internal/wire/srtpmimicry/srtpmimicry.go index 63e1188..7e22fc2 100644 --- a/internal/wire/srtpmimicry/srtpmimicry.go +++ b/internal/wire/srtpmimicry/srtpmimicry.go @@ -132,7 +132,7 @@ func (c *Conn) WrapInto(dst, payload []byte) (int, error) { // RTP-заголовок. dst[0] = rtpVersion dst[1] = rtpPT - seq := uint16(c.seq.Add(1) - 1) + seq := uint16(c.seq.Add(1) - 1) //nolint:gosec // RTP sequence number is intentionally mod 2^16 binary.BigEndian.PutUint16(dst[2:4], seq) ts := c.timestamp.Add(tsStep) - tsStep binary.BigEndian.PutUint32(dst[4:8], ts)