fix(lint): устранить замечания golangci-lint + переход на dockers_v2

Проход lint (52 → 0):
- vkauth/token: переименовать затенённый err (govet shadow x3)
- udprelay/loop: явный _ = conn.Close() (errcheck x2)
- captcha/manual: ReadHeaderTimeout + html.EscapeString (G112, G705)
- proxy/tcpfwd: хелпер nonNegUint64 для байтовых счётчиков io.Copy (G115)
- proxy/bondclient, wire/bondframe: bounds-проверки на len->uint16/uint32
- wire/srtpmimicry: задокументирована усечка RTP seq mod 2^16
- dnsdial/doh: bounds-проверка + binary.BigEndian.PutUint16
- captcha/slider: clamp luma до 255 перед конверсией uint8
- vkauth/types: обоснованный nolint для публичных учёток VK SDK

Новый пакет internal/randx — Intn/IntN/Float64 на crypto/rand, drop-in замена
math/rand. Мигрированы все места antifingerprint-jitter (~22) в captcha, slider,
namegen, vkauth, udprelay.

GoReleaser:
- bump GORELEASER_VERSION v2.5.0 → v2.15.4 в Taskfile и обоих CI-workflow;
  текущий синтаксис .goreleaser.yaml (archives.ids, format_overrides.formats)
  требует v2.8+
- миграция dockers + docker_manifests → dockers_v2 (единый блок, multi-platform
  buildx); Dockerfile.goreleaser использует конвенцию ARG TARGETPLATFORM
This commit is contained in:
samosvalishe
2026-05-23 01:37:42 +03:00
parent f8bc3f54e5
commit dd8233208c
20 changed files with 156 additions and 109 deletions
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -11,7 +11,7 @@ permissions:
env:
GO_VERSION: "1.25.0"
GORELEASER_VERSION: v2.5.0
GORELEASER_VERSION: v2.15.4
jobs:
goreleaser:
+1
View File
@@ -1,5 +1,6 @@
.idea
.claude
CLAUDE.md
*.exe
*.so
*.tmp
+15 -43
View File
@@ -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"
+5 -4
View File
@@ -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 \
+1 -1
View File
@@ -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:
+2 -2
View File
@@ -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)
+7 -3
View File
@@ -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, `<!DOCTYPE html><html><body style="font-family:sans-serif;padding:20px"><h2>Captcha proxy error</h2><p>%s %s</p><p>%v</p></body></html>`, r.Method, r.URL.String(), err)
_, _ = fmt.Fprintf(w, `<!DOCTYPE html><html><body style="font-family:sans-serif;padding:20px"><h2>Captcha proxy error</h2><p>%s %s</p><p>%s</p></body></html>`,
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
}
}
+24 -20
View File
@@ -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,
})
}
+7 -2
View File
@@ -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 {
+6 -5
View File
@@ -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)
}
+3 -3
View File
@@ -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()
+8 -8
View File
@@ -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-реквизиты.
+6 -1
View File
@@ -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
+6 -2
View File
@@ -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
+14 -4
View File
@@ -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)
}
+7 -7
View File
@@ -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
+36
View File
@@ -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)
}
+5 -1
View File
@@ -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
}
+1 -1
View File
@@ -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)