From 802ea7d0afae16b9eebca394a55731c47376b808 Mon Sep 17 00:00:00 2001 From: samosvalishe Date: Mon, 21 Sep 2026 13:36:18 +0300 Subject: [PATCH] =?UTF-8?q?ci:=20=D0=B7=D0=B0=D0=BF=D0=B8=D0=BD=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8=20gomobile,?= =?UTF-8?q?=20goimports=20=D0=B8=20docker-=D0=B4=D0=B5=D0=B9=D1=81=D1=82?= =?UTF-8?q?=D0=B2=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 4 +- Taskfile.yml | 3 +- internal/shutdown/shutdown.go | 52 +++++++++++++++++++++++ internal/wire/listen.go | 43 +++++++++++++++++++ internal/wire/listen_test.go | 79 +++++++++++++++++++++++++++++++++++ 6 files changed, 179 insertions(+), 4 deletions(-) create mode 100644 internal/shutdown/shutdown.go create mode 100644 internal/wire/listen.go create mode 100644 internal/wire/listen_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 631f054..4c50f8f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ env: GOLANGCI_VERSION: v2.12.2 GOVULN_VERSION: v1.6.0 GORELEASER_VERSION: v2.17.1 - GOMOBILE_VERSION: latest + GOMOBILE_VERSION: v0.0.0-20260821190718-4776eadac327 TASK_VERSION: 3.52.0 jobs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 86fc170..62e80a8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ permissions: env: GO_VERSION: "1.26.7" GORELEASER_VERSION: v2.17.1 - GOMOBILE_VERSION: latest + GOMOBILE_VERSION: v0.0.0-20260821190718-4776eadac327 TASK_VERSION: 3.52.0 jobs: @@ -120,7 +120,7 @@ jobs: echo "EOF" } >> "$GITHUB_OUTPUT" - - uses: docker/build-push-action@v6 + - uses: docker/build-push-action@v6.19.2 with: context: . file: docker/awg/Dockerfile diff --git a/Taskfile.yml b/Taskfile.yml index d9f2e85..657dfed 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -7,6 +7,7 @@ vars: LDFLAGS: -s -w -checklinkname=0 -X main.version={{.VERSION}} GOLANGCI_VERSION: v2.12.2 GOVULN_VERSION: v1.6.0 + GOIMPORTS_VERSION: v0.49.0 GORELEASER_VERSION: v2.17.1 TASK_VERSION: v3.52.0 @@ -125,7 +126,7 @@ tasks: tools:install: desc: Install dev tools (golangci-lint, govulncheck, goreleaser, goimports) cmds: - - go install golang.org/x/tools/cmd/goimports@latest + - go install golang.org/x/tools/cmd/goimports@{{.GOIMPORTS_VERSION}} - go install golang.org/x/vuln/cmd/govulncheck@{{.GOVULN_VERSION}} - go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@{{.GOLANGCI_VERSION}} - go install github.com/goreleaser/goreleaser/v2@{{.GORELEASER_VERSION}} diff --git a/internal/shutdown/shutdown.go b/internal/shutdown/shutdown.go new file mode 100644 index 0000000..67da227 --- /dev/null +++ b/internal/shutdown/shutdown.go @@ -0,0 +1,52 @@ +// Package shutdown связывает сигналы ОС с отменой контекста процесса. +package shutdown + +import ( + "context" + "os" + "os/signal" + "syscall" + "time" + + "github.com/samosvalishe/free-turn-proxy/internal/logx" +) + +// Grace - время на доработку после отмены контекста. +const Grace = 5 * time.Second + +// Watch отменяет ctx по SIGINT/SIGTERM. Возвращённый stop снимает обработчик при +// штатном выходе - без него процесс уходил бы в форс-выход с кодом 1 на каждой остановке. +func Watch(parent context.Context, log logx.Logger) (context.Context, func()) { + log = logx.OrNop(log) + ctx, cancel := context.WithCancel(parent) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT) + done := make(chan struct{}) + + go func() { + select { + case <-sig: + case <-done: + return + } + log.Infof("Terminating...") + cancel() + select { + case <-sig: + log.Warnf("Forced exit on second signal") + case <-done: + return + case <-time.After(Grace): + log.Warnf("Forced exit after %s shutdown timeout", Grace) + } + // Форс-выход сжигает defer-ы (в том числе снятие host-маршрутов) - только как крайняя мера. + os.Exit(1) + }() + + return ctx, func() { + signal.Stop(sig) + close(done) + cancel() + } +} diff --git a/internal/wire/listen.go b/internal/wire/listen.go new file mode 100644 index 0000000..56003a9 --- /dev/null +++ b/internal/wire/listen.go @@ -0,0 +1,43 @@ +package wire + +import ( + "fmt" + "net" + + dtlsnet "github.com/pion/dtls/v3/pkg/net" + pionudp "github.com/pion/transport/v4/udp" +) + +// NewServerCodec создаёт кодек для очередного принятого соединения: состояние RTP-сессии +// у каждого клиента своё, общий у профиля только ключ. +type NewServerCodec func() (Codec, error) + +// ListenCodec - единственная серверная обвязка на все профили: они отличались только +// именем пакета в текстах ошибок. +func ListenCodec(addr *net.UDPAddr, newCodec NewServerCodec) (dtlsnet.PacketListener, error) { + inner, err := pionudp.Listen("udp", addr) + if err != nil { + return nil, fmt.Errorf("wire: udp listen: %w", err) + } + return &packetListener{inner: dtlsnet.PacketListenerFromListener(inner), newCodec: newCodec}, nil +} + +type packetListener struct { + inner dtlsnet.PacketListener + newCodec NewServerCodec +} + +func (l *packetListener) Accept() (net.PacketConn, net.Addr, error) { + pc, addr, err := l.inner.Accept() + if err != nil { + return pc, addr, err + } + codec, err := l.newCodec() + if err != nil { + return nil, addr, err + } + return &RelayPacketConn{Relay: pc, Codec: codec}, addr, nil +} + +func (l *packetListener) Close() error { return l.inner.Close() } +func (l *packetListener) Addr() net.Addr { return l.inner.Addr() } diff --git a/internal/wire/listen_test.go b/internal/wire/listen_test.go new file mode 100644 index 0000000..68001b3 --- /dev/null +++ b/internal/wire/listen_test.go @@ -0,0 +1,79 @@ +package wire + +import ( + "crypto/rand" + "net" + "testing" + "time" +) + +// Серверная обвязка общая для всех профилей - проверяем каждый через реальный UDP. +func TestListenCodecRoundTrip(t *testing.T) { + t.Parallel() + + for _, profile := range []string{ProfileRTPOpus, ProfileRTPOpus2, ProfileRTPOpus3} { + t.Run(profile, func(t *testing.T) { + t.Parallel() + + key := make([]byte, 32) + if _, err := rand.Read(key); err != nil { + t.Fatal(err) + } + ln, err := Listen(profile, &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)}, key) + if err != nil { + t.Fatal(err) + } + defer func() { _ = ln.Close() }() + + raw, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)}) + if err != nil { + t.Fatal(err) + } + defer func() { _ = raw.Close() }() + + codec, err := NewClientCodec(profile, key) + if err != nil { + t.Fatal(err) + } + client := &RelayPacketConn{Relay: raw, Peer: ln.Addr(), Codec: codec} + + want := []byte("obfuscated payload") + if _, err = client.WriteTo(want, nil); err != nil { + t.Fatal(err) + } + + srv, _, err := ln.Accept() + if err != nil { + t.Fatal(err) + } + defer func() { _ = srv.Close() }() + + buf := make([]byte, 1500) + if err = srv.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil { + t.Fatal(err) + } + n, from, err := srv.ReadFrom(buf) + if err != nil { + t.Fatal(err) + } + if string(buf[:n]) != string(want) { + t.Fatalf("server got %q, want %q", buf[:n], want) + } + + // Peer у серверной стороны пустой: ответ обязан уйти отправителю. + if _, err = srv.WriteTo(buf[:n], from); err != nil { + t.Fatal(err) + } + if err = client.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil { + t.Fatal(err) + } + n, _, err = client.ReadFrom(buf) + if err != nil { + t.Fatal(err) + } + if string(buf[:n]) != string(want) { + t.Fatalf("client got %q, want %q", buf[:n], want) + } + }) + } +}