mirror of
https://github.com/hackdiaz-dev/free-turn-proxy.git
synced 2026-10-12 09:41:53 +02:00
refactor(wire): переименовать srtpmimicry → rtpopus, заменить bool -obf на -obf-profile
Готовим зонтик `internal/wire/` под будущие профили мимикрии (rtph264, vp8 и т.д.). Сейчас интерфейс/registry не вводим — родится при появлении второго профиля; пока только перепаковываем единственный профиль в собственный namespace. Пакет: - `internal/wire/srtpmimicry` → `internal/wire/rtpopus` - error-префикс `"wrap: ..."` → `"rtpopus: ..."` - wire-формат не тронут (заморожен — совместимость с задеплоенными пирами) CLI (breaking): - `-obf` (bool) удалён - добавлен `-obf-profile none|rtpopus` (default `none`) - `ObfOpts.Mode bool` → `Profile ObfProfile` + `Enabled()` helper - консты `ObfProfileNone`/`ObfProfileRTPOpus` - validation invalid-value error для `-obf-profile` Docker (breaking): - env `OBF_MODE=true` → `OBF_PROFILE=rtpopus` Callsites: - `common.NewClientWrap` → `NewClientObf` - vars `wc`/`relayWC` → `obfConn`, `wrapListener` → `obfListener` - log `obf=%t` → `obf-profile=%s` Доки + тесты подтянуты под новые имена.
This commit is contained in:
+4
-4
@@ -21,7 +21,7 @@ import (
|
||||
"github.com/samosvalishe/btp/internal/proxy/tcpfwd"
|
||||
"github.com/samosvalishe/btp/internal/proxy/udprelay"
|
||||
"github.com/samosvalishe/btp/internal/transport/dtlsdial"
|
||||
"github.com/samosvalishe/btp/internal/wire/srtpmimicry"
|
||||
"github.com/samosvalishe/btp/internal/wire/rtpopus"
|
||||
)
|
||||
|
||||
// version is populated at build time via -ldflags "-X main.version=...".
|
||||
@@ -64,7 +64,7 @@ func main() {
|
||||
appDialer := dnsdial.AppDialer(cfg.DNS.Mode)
|
||||
dnsdial.InstallGlobalResolver(cfg.DNS.Mode)
|
||||
if cfg.Obf.GenKey {
|
||||
key, gerr := srtpmimicry.GenKeyHex()
|
||||
key, gerr := rtpopus.GenKeyHex()
|
||||
if gerr != nil {
|
||||
logger.Errorf("gen-obf-key: %v", gerr)
|
||||
os.Exit(1)
|
||||
@@ -77,8 +77,8 @@ func main() {
|
||||
logger.Errorf("resolve peer addr: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if cfg.Obf.Mode {
|
||||
logger.Infof("OBF mode enabled: peer server must use matching -obf-key")
|
||||
if cfg.Obf.Enabled() {
|
||||
logger.Infof("OBF profile=%s: peer server must use matching -obf-profile and -obf-key", cfg.Obf.Profile)
|
||||
}
|
||||
|
||||
var connectedStreams atomic.Int32
|
||||
|
||||
+12
-12
@@ -18,7 +18,7 @@ import (
|
||||
"github.com/samosvalishe/btp/internal/proxy/tcpfwdserver"
|
||||
"github.com/samosvalishe/btp/internal/proxy/udpserver"
|
||||
"github.com/samosvalishe/btp/internal/transport/dtlsdial"
|
||||
"github.com/samosvalishe/btp/internal/wire/srtpmimicry"
|
||||
"github.com/samosvalishe/btp/internal/wire/rtpopus"
|
||||
)
|
||||
|
||||
// version is populated at build time via -ldflags "-X main.version=...".
|
||||
@@ -34,7 +34,7 @@ func main() {
|
||||
logger.Infof("btp server version=%s", version)
|
||||
|
||||
if cfg.Obf.GenKey {
|
||||
key, gerr := srtpmimicry.GenKeyHex()
|
||||
key, gerr := rtpopus.GenKeyHex()
|
||||
if gerr != nil {
|
||||
logger.Errorf("gen-obf-key: %v", gerr)
|
||||
os.Exit(1)
|
||||
@@ -65,10 +65,10 @@ func main() {
|
||||
logger.Errorf("resolve listen addr: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
logger.Infof("Starting server listen=%s connect=%s mode=%s obf=%t bond-autodetect=true",
|
||||
cfg.Proxy.Listen, cfg.Proxy.Connect, cfg.Proxy.Mode, cfg.Obf.Mode)
|
||||
if !cfg.Obf.Mode {
|
||||
logger.Warnf("running without -obf: any client reaching %s can relay to %s (no shared-key auth)", cfg.Proxy.Listen, cfg.Proxy.Connect)
|
||||
logger.Infof("Starting server listen=%s connect=%s mode=%s obf-profile=%s bond-autodetect=true",
|
||||
cfg.Proxy.Listen, cfg.Proxy.Connect, cfg.Proxy.Mode, cfg.Obf.Profile)
|
||||
if !cfg.Obf.Enabled() {
|
||||
logger.Warnf("running with -obf-profile=none: any client reaching %s can relay to %s (no shared-key auth)", cfg.Proxy.Listen, cfg.Proxy.Connect)
|
||||
}
|
||||
|
||||
certificate, genErr := dtlsdial.GenerateSelfSignedCert()
|
||||
@@ -84,14 +84,14 @@ func main() {
|
||||
dtls.WithConnectionIDGenerator(dtls.RandomCIDGenerator(8)),
|
||||
}
|
||||
var listener net.Listener
|
||||
if cfg.Obf.Mode {
|
||||
logger.Infof("OBF mode enabled: listener only accepts clients with matching -obf-key")
|
||||
wrapListener, werr := srtpmimicry.Listen(addr, cfg.Obf.Key)
|
||||
if werr != nil {
|
||||
logger.Errorf("wrap listen: %v", werr)
|
||||
if cfg.Obf.Enabled() {
|
||||
logger.Infof("OBF profile=%s: listener only accepts clients with matching -obf-profile and -obf-key", cfg.Obf.Profile)
|
||||
obfListener, oerr := rtpopus.Listen(addr, cfg.Obf.Key)
|
||||
if oerr != nil {
|
||||
logger.Errorf("obf listen: %v", oerr)
|
||||
os.Exit(1)
|
||||
}
|
||||
listener, err = dtls.NewListenerWithOptions(wrapListener, dtlsOpts...)
|
||||
listener, err = dtls.NewListenerWithOptions(obfListener, dtlsOpts...)
|
||||
} else {
|
||||
listener, err = dtls.ListenWithOptions("udp", addr, dtlsOpts...)
|
||||
}
|
||||
|
||||
+1
-1
@@ -14,6 +14,6 @@ services:
|
||||
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_PROFILE: "rtpopus" # none (default) | rtpopus
|
||||
# OBF_KEY задаётся в .env (не коммитьте ключ в git)
|
||||
# DEBUG: "false"
|
||||
|
||||
@@ -15,9 +15,9 @@ if [ "${MODE}" = "tcp" ]; then
|
||||
set -- "$@" -mode tcp
|
||||
fi
|
||||
|
||||
if [ "${OBF_MODE}" = "true" ]; then
|
||||
OBF="${OBF_KEY:?OBF_KEY is required when OBF_MODE=true}"
|
||||
set -- "$@" -obf -obf-key "$OBF"
|
||||
if [ -n "${OBF_PROFILE}" ] && [ "${OBF_PROFILE}" != "none" ]; then
|
||||
OBF="${OBF_KEY:?OBF_KEY is required when OBF_PROFILE != none}"
|
||||
set -- "$@" -obf-profile "$OBF_PROFILE" -obf-key "$OBF"
|
||||
fi
|
||||
|
||||
if [ "${DEBUG}" = "true" ]; then
|
||||
|
||||
+3
-3
@@ -10,7 +10,7 @@ docker compose logs -f
|
||||
|
||||
По умолчанию: WireGuard backend `127.0.0.1:51820`, host network, порт `56000/udp`.
|
||||
|
||||
Для Xray/sing-box: `MODE=tcp` и смените `CONNECT_ADDR` на `127.0.0.1:443`. Для SRTP-мимикрии: `OBF_MODE=true` и `OBF_KEY=<64-hex>`.
|
||||
Для Xray/sing-box: `MODE=tcp` и смените `CONNECT_ADDR` на `127.0.0.1:443`. Для обфускации payload: `OBF_PROFILE=rtpopus` и `OBF_KEY=<64-hex>`.
|
||||
|
||||
## Docker Run
|
||||
|
||||
@@ -39,8 +39,8 @@ docker run -d --name btp -p 56000:56000/udp --restart unless-stopped \
|
||||
| `CONNECT_ADDR` | **обязательна** | backend сервера |
|
||||
| `LISTEN_ADDR` | `0.0.0.0:56000` | адрес прослушивания |
|
||||
| `MODE` | `udp` | режим туннеля: `udp` \| `tcp` |
|
||||
| `OBF_MODE` | `false` | включает `-obf` |
|
||||
| `OBF_KEY` | пусто | значение `-obf-key` |
|
||||
| `OBF_PROFILE` | `none` | значение `-obf-profile`: `none` \| `rtpopus` |
|
||||
| `OBF_KEY` | пусто | значение `-obf-key` (обязателен при `OBF_PROFILE != none`) |
|
||||
| `DEBUG` | `false` | включает `-debug` |
|
||||
|
||||
Сборка образа:
|
||||
|
||||
+4
-4
@@ -14,8 +14,8 @@
|
||||
| `-bond` | `false` | распределять одно TCP-соединение по всем активным smux-сессиям (только с `-mode tcp`) |
|
||||
| `-turn` | из creds | переопределить IP TURN-сервера |
|
||||
| `-port` | из creds | переопределить порт TURN-сервера |
|
||||
| `-obf` | `false` | SRTP-мимикрия payload |
|
||||
| `-obf-key` | пусто | общий ключ, 32 байта hex (64 символа) |
|
||||
| `-obf-profile` | `none` | wire-профиль обфускации payload: `none` \| `rtpopus` (RTP/opus + ChaCha20-Poly1305 AEAD) |
|
||||
| `-obf-key` | пусто | общий ключ для `-obf-profile != none`, 32 байта hex (64 символа) |
|
||||
| `-gen-obf-key` | `false` | напечатать новый ключ и выйти |
|
||||
| `-manual-captcha` | `false` | сразу ручной режим captcha (только `-provider vk`) |
|
||||
| `-streams-per-cred` | `10` | потоков на один кеш VK-учёток (только `-provider vk`) |
|
||||
@@ -30,8 +30,8 @@
|
||||
| `-listen` | `0.0.0.0:56000` | адрес прослушивания `ip:port` |
|
||||
| `-connect` | **обязательный** | локальный backend `host:port` (WG `127.0.0.1:51820` / Xray `127.0.0.1:443`) |
|
||||
| `-mode` | `udp` | режим туннеля: `udp` \| `tcp` (bond автоопределяется) |
|
||||
| `-obf` | `false` | SRTP-мимикрия payload |
|
||||
| `-obf-key` | пусто | общий ключ, 32 байта hex |
|
||||
| `-obf-profile` | `none` | wire-профиль обфускации payload: `none` \| `rtpopus` (RTP/opus + ChaCha20-Poly1305 AEAD) |
|
||||
| `-obf-key` | пусто | общий ключ для `-obf-profile != none`, 32 байта hex |
|
||||
| `-gen-obf-key` | `false` | напечатать новый ключ и выйти |
|
||||
| `-debug` | `false` | debug-логи |
|
||||
|
||||
|
||||
+7
-4
@@ -11,7 +11,7 @@ backend (WireGuard UDP | TCP service)
|
||||
↑
|
||||
[app proxy mode] ← UDP-relay (-mode udp) | TCP-forward (-mode tcp) | TCP-forward+bond (-mode tcp -bond)
|
||||
↑
|
||||
[srtpmimicry] (optional, AEAD обфускация поверх payload)
|
||||
[obf wire profile] (optional, AEAD обфускация поверх payload; сейчас только rtpopus)
|
||||
↑
|
||||
[DTLS] ← обфускация (не security): VK content-filter ожидает DTLS handshake
|
||||
↑
|
||||
@@ -38,8 +38,11 @@ Pipeline (proxy/udprelay, proxy/tcpfwd) работает только через
|
||||
### DTLS — это **обфускация, не security**
|
||||
VK content-filter дропает payload, который не похож на legit DTLS-handshake. Мы используем `pion/dtls` чтобы пройти фильтр. Шифрование DTLS — побочный эффект, не цель.
|
||||
|
||||
### srtpmimicry
|
||||
Пакет `internal/wire/srtpmimicry`. CLI флаг `-obf`. Это **ChaCha20-Poly1305 AEAD** с RTP-like wire-header (mimicry). Включается опционально поверх DTLS — даёт дополнительный слой обфускации с RTP-видимостью на проводе. Не путать с Go-термином wrap (`errors.Wrap`).
|
||||
### obf wire profile
|
||||
Зонтик `internal/wire/` объединяет wire-форматы. Профили обфускации выбираются флагом `-obf-profile` (default `none`):
|
||||
- **`rtpopus`** (`internal/wire/rtpopus`): **ChaCha20-Poly1305 AEAD** с RTP/opus wire-header (mimicry). RTP-видимость на проводе обходит VK TURN content-filter.
|
||||
|
||||
Профили задуманы расширяемыми (rtph264, vp8 и т.д.) — общий интерфейс введём при появлении второго профиля. Wire-формат `rtpopus` заморожен (требуется побитовая совместимость с задеплоенными пирами). Не путать с Go-термином wrap (`errors.Wrap`) — методы `WrapInto`/`Unwrap` означают энкод/декод wire-фрейма.
|
||||
|
||||
### app proxy mode (3 значения)
|
||||
Что копируется поверх DTLS:
|
||||
@@ -87,7 +90,7 @@ Linux user-mode environment для iOS (через usermode x86 emulation). Па
|
||||
- `-transport tcp|udp` — транспорт до TURN (TCP/TLS или UDP).
|
||||
- `-mode udp|tcp` — app proxy mode (UDP-relay|TCP-forward).
|
||||
- `-bond` — bonding внутри TCP-forward (только client).
|
||||
- `-obf` — srtpmimicry-обфускация.
|
||||
- `-obf-profile` — wire-профиль обфускации (`none` | `rtpopus`).
|
||||
- `-dns-mode` — транспорт резолвера клиента (plain|doh|auto).
|
||||
|
||||
### captcha (auto / manual)
|
||||
|
||||
+10
-4
@@ -23,9 +23,15 @@
|
||||
|
||||
Серверного `-bond` нет — сервер автоопределяет bond по magic-префиксу в стриме.
|
||||
|
||||
## OBF (SRTP-Мимикрия)
|
||||
## OBF (wire-профили обфускации)
|
||||
|
||||
`-obf` маскирует TURN-payload под SRTP: RTP/opus-заголовок + ChaCha20-Poly1305 AEAD на теле. Не защита (DTLS уже шифрует), а обфускация под голос — иначе VK content-filter дропает. Ключ должен совпадать.
|
||||
`-obf-profile` выбирает wire-профиль маскировки TURN-payload. Не защита (DTLS уже шифрует), а обфускация — иначе VK content-filter дропает payload, не похожий на голосовой трафик. Профиль и ключ должны совпадать на клиенте и сервере.
|
||||
|
||||
Доступные профили:
|
||||
- **`none`** (default) — обфускация выключена.
|
||||
- **`rtpopus`** — RTP/opus-заголовок + ChaCha20-Poly1305 AEAD на теле.
|
||||
|
||||
Будущие профили (rtph264, vp8 и т.д.) добавятся по мере необходимости.
|
||||
|
||||
Сгенерировать ключ:
|
||||
|
||||
@@ -36,8 +42,8 @@
|
||||
Запуск:
|
||||
|
||||
```bash
|
||||
./server ... -obf -obf-key <64-hex>
|
||||
./client ... -obf -obf-key <64-hex>
|
||||
./server ... -obf-profile rtpopus -obf-key <64-hex>
|
||||
./client ... -obf-profile rtpopus -obf-key <64-hex>
|
||||
```
|
||||
|
||||
## TURN Транспорт
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ VK Calls API. Перебирает встроенные `app_id/app_secret`, п
|
||||
|
||||
- DNS-стратегия резолвинга control-plane API (`internal/client/dnsdial`) — ортогональна провайдеру. VK-провайдер использует её для VK API, другой провайдер может использовать для своего HTTP API или не использовать вовсе.
|
||||
- Транспорт до TURN-реле (`-transport tcp|udp`) — определяется на pipeline-уровне, провайдер только выдаёт `host:port`.
|
||||
- Application-mode (`-mode udp|tcp`, `-bond`, `-obf`) — поверх TURN, не зависит от источника creds.
|
||||
- Application-mode (`-mode udp|tcp`, `-bond`, `-obf-profile`) — поверх TURN, не зависит от источника creds.
|
||||
|
||||
## Известные ограничения текущей абстракции
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
- VK часто требует captcha → `-manual-captcha`, затем обычный запуск.
|
||||
- Клиент висит на получении TURN-данных → ссылка VK Calls умерла / звонок завершён для всех.
|
||||
- Docker bridge mode → `CONNECT_ADDR=127.0.0.1:...` указывает внутрь контейнера. Используйте `--network host` или IP хоста.
|
||||
- `-obf` включён → ключ должен совпадать на клиенте и сервере.
|
||||
- `-obf-profile` != `none` → профиль и ключ должны совпадать на клиенте и сервере.
|
||||
- Bond на сервере не нужен — сервер определяет bond автоматически. Используйте только `-bond` на клиенте с `-mode tcp`.
|
||||
|
||||
+46
-17
@@ -16,7 +16,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/samosvalishe/btp/internal/transport/kcptun"
|
||||
"github.com/samosvalishe/btp/internal/wire/srtpmimicry"
|
||||
"github.com/samosvalishe/btp/internal/wire/rtpopus"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -45,13 +45,26 @@ type TURNOpts struct {
|
||||
N int // -n: число TURN-потоков (только клиент)
|
||||
}
|
||||
|
||||
// ObfOpts — опции обфускации TURN-payload (SRTP-mimicry).
|
||||
// ObfProfile выбирает wire-профиль обфускации TURN-payload.
|
||||
// Профили живут в internal/wire/<profile>/ — сейчас только rtpopus,
|
||||
// под добавление новых (rtph264, vp8 и т.д.).
|
||||
type ObfProfile string
|
||||
|
||||
const (
|
||||
ObfProfileNone ObfProfile = "none" // обфускация отключена
|
||||
ObfProfileRTPOpus ObfProfile = "rtpopus" // RTP/opus + ChaCha20-Poly1305 AEAD
|
||||
)
|
||||
|
||||
// ObfOpts — опции обфускации TURN-payload.
|
||||
type ObfOpts struct {
|
||||
Mode bool // -obf: включить SRTP-mimicry AEAD-обёртку
|
||||
Key []byte // -obf-key (декодированный): 32-байтовый общий ключ; nil если Mode=false
|
||||
GenKey bool // -gen-obf-key: напечатать новый ключ и выйти
|
||||
Profile ObfProfile // -obf-profile: none (default) | rtpopus
|
||||
Key []byte // -obf-key (декодированный): 32-байтовый общий ключ; nil если Profile=none
|
||||
GenKey bool // -gen-obf-key: напечатать новый ключ и выйти
|
||||
}
|
||||
|
||||
// Enabled возвращает true когда выбран реальный профиль обфускации.
|
||||
func (o ObfOpts) Enabled() bool { return o.Profile != ObfProfileNone }
|
||||
|
||||
// ProxyOpts — опции прокси прикладного уровня.
|
||||
type ProxyOpts struct {
|
||||
Mode ProxyMode // udp | tcpfwd | tcpfwd-bond (сервер: udp | tcpfwd)
|
||||
@@ -132,9 +145,9 @@ func ParseClient(args []string, errOut io.Writer) (*Client, error) {
|
||||
transportFlag := fs.String("transport", "tcp", "транспорт до TURN-реле: tcp (TCP/TLS, default) | udp")
|
||||
modeFlag := fs.String("mode", "udp", "режим туннеля: udp (UDP-релей для WireGuard, default) | tcp (TCP-форвардер для Xray/sing-box)")
|
||||
bondFlag := fs.Bool("bond", false, "распределять одно TCP-соединение по всем активным smux-сессиям (только с -mode tcp)")
|
||||
wrapMode := fs.Bool("obf", false, "маскировать TURN-payload под SRTP (RTP/opus + ChaCha20-Poly1305 AEAD) для обхода content-filter VK; ключ должен совпадать на клиенте и сервере")
|
||||
wrapKeyHex := fs.String("obf-key", "", "общий ключ для -obf, 32 байта в hex (64 символа)")
|
||||
genWrapKey := fs.Bool("gen-obf-key", false, "напечатать новый ключ для -obf-key и выйти")
|
||||
obfProfileRaw := fs.String("obf-profile", string(ObfProfileNone), "wire-профиль обфускации TURN-payload: none (default) | rtpopus (RTP/opus + ChaCha20-Poly1305 AEAD для обхода content-filter VK); должен совпадать с сервером")
|
||||
obfKeyHex := fs.String("obf-key", "", "общий ключ для -obf-profile != none, 32 байта в hex (64 символа)")
|
||||
genObfKey := fs.Bool("gen-obf-key", false, "напечатать новый ключ для -obf-key и выйти")
|
||||
streamsPerCredFlag := fs.Int("streams-per-cred", defaultStreamsPerCache, "сколько TURN-потоков делят один кеш VK-учёток (только -provider vk)")
|
||||
debugFlag := fs.Bool("debug", false, "включить подробные debug-логи")
|
||||
manualCaptchaFlag := fs.Bool("manual-captcha", false, "пропустить авто-решение VK captcha и сразу открыть ручной режим в локальном браузере (только -provider vk)")
|
||||
@@ -153,8 +166,8 @@ func ParseClient(args []string, errOut io.Writer) (*Client, error) {
|
||||
N: *n,
|
||||
},
|
||||
Obf: ObfOpts{
|
||||
Mode: *wrapMode,
|
||||
GenKey: *genWrapKey,
|
||||
Profile: ObfProfile(*obfProfileRaw),
|
||||
GenKey: *genObfKey,
|
||||
},
|
||||
Proxy: ProxyOpts{
|
||||
Mode: clientProxyMode(*modeFlag, *bondFlag),
|
||||
@@ -226,7 +239,10 @@ func ParseClient(args []string, errOut io.Writer) (*Client, error) {
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid -provider value %q: must be %s", c.Provider.Name, ProviderVK)
|
||||
}
|
||||
key, err := srtpmimicry.DecodeKey(c.Obf.Mode, *wrapKeyHex)
|
||||
if err := validateObfProfile(c.Obf.Profile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, err := rtpopus.DecodeKey(c.Obf.Enabled(), *obfKeyHex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -248,9 +264,9 @@ func ParseServer(args []string, errOut io.Writer) (*Server, error) {
|
||||
listen := fs.String("listen", "0.0.0.0:56000", "локальный адрес прослушивания ip:port")
|
||||
connect := fs.String("connect", "", "адрес локального бэкенда, host:port (обязательно: WireGuard 127.0.0.1:51820 или Xray 127.0.0.1:443)")
|
||||
modeFlag := fs.String("mode", "udp", "режим туннеля: udp (UDP-релей для WireGuard, default) | tcp (TCP-форвардер для Xray/sing-box; bond определяется автоматически)")
|
||||
wrapMode := fs.Bool("obf", false, "маскировать TURN-payload под SRTP (RTP/opus + ChaCha20-Poly1305 AEAD) для обхода content-filter VK; ключ должен совпадать с клиентом")
|
||||
wrapKeyHex := fs.String("obf-key", "", "общий ключ для -obf, 32 байта в hex (64 символа)")
|
||||
genWrapKey := fs.Bool("gen-obf-key", false, "напечатать новый ключ для -obf-key и выйти")
|
||||
obfProfileRaw := fs.String("obf-profile", string(ObfProfileNone), "wire-профиль обфускации TURN-payload: none (default) | rtpopus (RTP/opus + ChaCha20-Poly1305 AEAD для обхода content-filter VK); должен совпадать с клиентом")
|
||||
obfKeyHex := fs.String("obf-key", "", "общий ключ для -obf-profile != none, 32 байта в hex (64 символа)")
|
||||
genObfKey := fs.Bool("gen-obf-key", false, "напечатать новый ключ для -obf-key и выйти")
|
||||
debugFlag := fs.Bool("debug", false, "включить подробные debug-логи")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
@@ -259,8 +275,8 @@ func ParseServer(args []string, errOut io.Writer) (*Server, error) {
|
||||
|
||||
s := &Server{
|
||||
Obf: ObfOpts{
|
||||
Mode: *wrapMode,
|
||||
GenKey: *genWrapKey,
|
||||
Profile: ObfProfile(*obfProfileRaw),
|
||||
GenKey: *genObfKey,
|
||||
},
|
||||
Proxy: ProxyOpts{
|
||||
Mode: serverProxyMode(*modeFlag),
|
||||
@@ -289,7 +305,10 @@ func ParseServer(args []string, errOut io.Writer) (*Server, error) {
|
||||
if s.Proxy.Connect == "" {
|
||||
return nil, fmt.Errorf("server address is required")
|
||||
}
|
||||
key, err := srtpmimicry.DecodeKey(s.Obf.Mode, *wrapKeyHex)
|
||||
if err := validateObfProfile(s.Obf.Profile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, err := rtpopus.DecodeKey(s.Obf.Enabled(), *obfKeyHex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -298,6 +317,16 @@ func ParseServer(args []string, errOut io.Writer) (*Server, error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// validateObfProfile проверяет что -obf-profile содержит известное значение.
|
||||
func validateObfProfile(p ObfProfile) error {
|
||||
switch p {
|
||||
case ObfProfileNone, ObfProfileRTPOpus:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid -obf-profile value %q: must be %s | %s", p, ObfProfileNone, ObfProfileRTPOpus)
|
||||
}
|
||||
}
|
||||
|
||||
func clientProxyMode(mode string, bond bool) ProxyMode {
|
||||
switch {
|
||||
case mode == "tcp" && bond:
|
||||
|
||||
@@ -40,7 +40,13 @@ func TestParseClient_Defaults(t *testing.T) {
|
||||
t.Errorf("VK.Link: %q (expected abcdef)", c.VK.Link)
|
||||
}
|
||||
if c.Obf.Key != nil {
|
||||
t.Errorf("Obf.Key should be nil when -obf absent")
|
||||
t.Errorf("Obf.Key should be nil when -obf-profile absent")
|
||||
}
|
||||
if c.Obf.Profile != ObfProfileNone {
|
||||
t.Errorf("Obf.Profile default: %q (expected %q)", c.Obf.Profile, ObfProfileNone)
|
||||
}
|
||||
if c.Obf.Enabled() {
|
||||
t.Errorf("Obf.Enabled() should be false when profile=none")
|
||||
}
|
||||
if c.Proxy.Mode != ProxyModeUDP {
|
||||
t.Errorf("Proxy.Mode default: %q (expected udp)", c.Proxy.Mode)
|
||||
@@ -92,15 +98,15 @@ func TestParseClient_BondWithoutTCPMode(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseClient_ObfMissingKey(t *testing.T) {
|
||||
args := append(validClientArgs(), "-obf")
|
||||
args := append(validClientArgs(), "-obf-profile", "rtpopus")
|
||||
_, err := ParseClient(args, io.Discard)
|
||||
if err == nil || !strings.Contains(err.Error(), "-obf requires -obf-key") {
|
||||
if err == nil || !strings.Contains(err.Error(), "-obf-key") {
|
||||
t.Errorf("expected obf-key error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseClient_ObfKeyOK(t *testing.T) {
|
||||
args := append(validClientArgs(), "-obf", "-obf-key", strings.Repeat("ab", 32))
|
||||
args := append(validClientArgs(), "-obf-profile", "rtpopus", "-obf-key", strings.Repeat("ab", 32))
|
||||
c, err := ParseClient(args, io.Discard)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -108,6 +114,20 @@ func TestParseClient_ObfKeyOK(t *testing.T) {
|
||||
if len(c.Obf.Key) != 32 {
|
||||
t.Errorf("Obf.Key len: %d", len(c.Obf.Key))
|
||||
}
|
||||
if c.Obf.Profile != ObfProfileRTPOpus {
|
||||
t.Errorf("Obf.Profile: %q (expected %q)", c.Obf.Profile, ObfProfileRTPOpus)
|
||||
}
|
||||
if !c.Obf.Enabled() {
|
||||
t.Errorf("Obf.Enabled() should be true when profile=rtpopus")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseClient_ObfProfileInvalid(t *testing.T) {
|
||||
args := append(validClientArgs(), "-obf-profile", "bogus")
|
||||
_, err := ParseClient(args, io.Discard)
|
||||
if err == nil || !strings.Contains(err.Error(), "invalid -obf-profile") {
|
||||
t.Errorf("expected invalid obf-profile error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseClient_StreamsPerCredNonPositive(t *testing.T) {
|
||||
@@ -215,27 +235,37 @@ func TestParseServer_MissingConnect(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseServer_ObfMissingKey(t *testing.T) {
|
||||
_, err := ParseServer([]string{"-connect", "x:1", "-obf"}, io.Discard)
|
||||
if err == nil || !strings.Contains(err.Error(), "-obf requires -obf-key") {
|
||||
_, err := ParseServer([]string{"-connect", "x:1", "-obf-profile", "rtpopus"}, io.Discard)
|
||||
if err == nil || !strings.Contains(err.Error(), "-obf-key") {
|
||||
t.Errorf("expected obf-key error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseServer_ObfKeyBadHex(t *testing.T) {
|
||||
_, err := ParseServer([]string{"-connect", "x:1", "-obf", "-obf-key", "zz"}, io.Discard)
|
||||
_, err := ParseServer([]string{"-connect", "x:1", "-obf-profile", "rtpopus", "-obf-key", "zz"}, io.Discard)
|
||||
if err == nil || !strings.Contains(err.Error(), "invalid hex") {
|
||||
t.Errorf("expected hex error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseServer_ObfKeyOK(t *testing.T) {
|
||||
s, err := ParseServer([]string{"-connect", "x:1", "-obf", "-obf-key", strings.Repeat("cd", 32)}, io.Discard)
|
||||
s, err := ParseServer([]string{"-connect", "x:1", "-obf-profile", "rtpopus", "-obf-key", strings.Repeat("cd", 32)}, io.Discard)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(s.Obf.Key) != 32 {
|
||||
t.Errorf("Obf.Key len: %d", len(s.Obf.Key))
|
||||
}
|
||||
if s.Obf.Profile != ObfProfileRTPOpus {
|
||||
t.Errorf("Obf.Profile: %q (expected %q)", s.Obf.Profile, ObfProfileRTPOpus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseServer_ObfProfileInvalid(t *testing.T) {
|
||||
_, err := ParseServer([]string{"-connect", "x:1", "-obf-profile", "bogus"}, io.Discard)
|
||||
if err == nil || !strings.Contains(err.Error(), "invalid -obf-profile") {
|
||||
t.Errorf("expected invalid obf-profile error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseServer_GenObfKeySkipsConnectCheck(t *testing.T) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package common содержит хелперы, общие для udprelay и tcpfwd
|
||||
// (TURN-dial + создание wrap-кодека). Два режима прокси по-разному компонуют DTLS и
|
||||
// srtpmimicry, поэтому полная абстракция Engine/Handler намеренно не вводится —
|
||||
// (TURN-dial + создание obf-кодека). Два режима прокси по-разному компонуют DTLS
|
||||
// и rtpopus, поэтому полная абстракция Engine/Handler намеренно не вводится —
|
||||
// пакет собирает только действительно идентичный код.
|
||||
package common
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"net"
|
||||
|
||||
"github.com/samosvalishe/btp/internal/transport/turndial"
|
||||
"github.com/samosvalishe/btp/internal/wire/srtpmimicry"
|
||||
"github.com/samosvalishe/btp/internal/wire/rtpopus"
|
||||
)
|
||||
|
||||
// GetCredsFunc разрешает TURN-реквизиты для streamID. Реализуется provider'ом
|
||||
@@ -33,11 +33,11 @@ func DialTURN(ctx context.Context, host, port string, udp bool, peer *net.UDPAdd
|
||||
}, peer, user, pass, rawURL)
|
||||
}
|
||||
|
||||
// NewClientWrap возвращает клиентский srtpmimicry.Conn если key нужной длины,
|
||||
// иначе (nil, nil) — wrap отключён. Ошибки NewConn пробрасываются вызывающему.
|
||||
func NewClientWrap(key []byte) (*srtpmimicry.Conn, error) {
|
||||
if len(key) != srtpmimicry.KeyLen {
|
||||
// NewClientObf возвращает клиентский rtpopus.Conn если key нужной длины,
|
||||
// иначе (nil, nil) — обфускация отключена. Ошибки NewConn пробрасываются вызывающему.
|
||||
func NewClientObf(key []byte) (*rtpopus.Conn, error) {
|
||||
if len(key) != rtpopus.KeyLen {
|
||||
return nil, nil
|
||||
}
|
||||
return srtpmimicry.NewConn(key, false)
|
||||
return rtpopus.NewConn(key, false)
|
||||
}
|
||||
|
||||
@@ -14,14 +14,14 @@ import (
|
||||
"github.com/samosvalishe/btp/internal/stats"
|
||||
"github.com/samosvalishe/btp/internal/transport/dtlsdial"
|
||||
"github.com/samosvalishe/btp/internal/transport/kcptun"
|
||||
"github.com/samosvalishe/btp/internal/wire/srtpmimicry"
|
||||
"github.com/samosvalishe/btp/internal/wire/rtpopus"
|
||||
"github.com/xtaci/smux"
|
||||
)
|
||||
|
||||
// GetCredsFunc реэкспортирован из common, чтобы вызывающие не выходили за пределы импортов пакета.
|
||||
type GetCredsFunc = common.GetCredsFunc
|
||||
|
||||
// Params — конфигурация TURN/wrap для пула.
|
||||
// Params — конфигурация TURN/obf для пула.
|
||||
type Params struct {
|
||||
Host string
|
||||
Port string
|
||||
@@ -241,12 +241,12 @@ func createSmuxSession(ctx context.Context, deps *Deps, params *Params, peer *ne
|
||||
deps.log().Debugf("[session %d] TURN server IP: %s", id, stream.ServerUDPAddr.IP)
|
||||
deps.log().Debugf("relayed-address=%s", relayConn.LocalAddr().String())
|
||||
|
||||
relayWC, err := common.NewClientWrap(params.ObfKey)
|
||||
obfConn, err := common.NewClientObf(params.ObfKey)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, fmt.Errorf("wrap init: %w", err)
|
||||
return nil, nil, fmt.Errorf("obf init: %w", err)
|
||||
}
|
||||
dtlsPC := &srtpmimicry.RelayPacketConn{Relay: relayConn, Peer: peer, Conn: relayWC}
|
||||
dtlsPC := &rtpopus.RelayPacketConn{Relay: relayConn, Peer: peer, Conn: obfConn}
|
||||
dtlsConn, err := deps.DTLSDialer.Dial(ctx, dtlsPC, peer)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"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"
|
||||
"github.com/samosvalishe/btp/internal/wire/rtpopus"
|
||||
)
|
||||
|
||||
// DTLSLoop поддерживает единственное DTLS-подключение для streamID, перезапуская
|
||||
@@ -255,9 +255,9 @@ func oneTURN(ctx context.Context, deps *Deps, params *Params, peer *net.UDPAddr,
|
||||
}
|
||||
})
|
||||
var internalPipeAddr atomic.Value
|
||||
wc, wcErr := common.NewClientWrap(params.ObfKey)
|
||||
if wcErr != nil {
|
||||
deps.log().Errorf("[STREAM %d] WRAP init failed: %v", streamID, wcErr)
|
||||
obfConn, obfErr := common.NewClientObf(params.ObfKey)
|
||||
if obfErr != nil {
|
||||
deps.log().Errorf("[STREAM %d] OBF init failed: %v", streamID, obfErr)
|
||||
turncancel()
|
||||
return
|
||||
}
|
||||
@@ -266,8 +266,8 @@ func oneTURN(ctx context.Context, deps *Deps, params *Params, peer *net.UDPAddr,
|
||||
defer turncancel()
|
||||
buf := make([]byte, 1600)
|
||||
var wireBuf []byte
|
||||
if wc != nil {
|
||||
wireBuf = make([]byte, srtpmimicry.MaxWire(len(buf)))
|
||||
if obfConn != nil {
|
||||
wireBuf = make([]byte, rtpopus.MaxWire(len(buf)))
|
||||
}
|
||||
for {
|
||||
if turnctx.Err() != nil {
|
||||
@@ -284,10 +284,10 @@ func oneTURN(ctx context.Context, deps *Deps, params *Params, peer *net.UDPAddr,
|
||||
internalPipeAddr.Store(addr1)
|
||||
|
||||
out := buf[:n]
|
||||
if wc != nil {
|
||||
written, wrapErr := wc.WrapInto(wireBuf, out)
|
||||
if wrapErr != nil {
|
||||
deps.log().Errorf("[STREAM %d] WRAP failed: %v", streamID, wrapErr)
|
||||
if obfConn != nil {
|
||||
written, wErr := obfConn.WrapInto(wireBuf, out)
|
||||
if wErr != nil {
|
||||
deps.log().Errorf("[STREAM %d] OBF wrap failed: %v", streamID, wErr)
|
||||
return
|
||||
}
|
||||
out = wireBuf[:written]
|
||||
@@ -304,8 +304,8 @@ func oneTURN(ctx context.Context, deps *Deps, params *Params, peer *net.UDPAddr,
|
||||
wg.Go(func() {
|
||||
defer turncancel()
|
||||
readBufLen := 1600
|
||||
if wc != nil {
|
||||
readBufLen = srtpmimicry.MaxWire(1600)
|
||||
if obfConn != nil {
|
||||
readBufLen = rtpopus.MaxWire(1600)
|
||||
}
|
||||
buf := make([]byte, readBufLen)
|
||||
plain := make([]byte, 1600)
|
||||
@@ -321,10 +321,10 @@ func oneTURN(ctx context.Context, deps *Deps, params *Params, peer *net.UDPAddr,
|
||||
|
||||
if addr, ok := addr1.(net.Addr); ok {
|
||||
payload := buf[:n]
|
||||
if wc != nil {
|
||||
m, wrapErr := wc.Unwrap(payload, plain)
|
||||
if wrapErr != nil {
|
||||
deps.log().Errorf("[STREAM %d] UNWRAP failed: %v (n=%d)", streamID, wrapErr, n)
|
||||
if obfConn != nil {
|
||||
m, uErr := obfConn.Unwrap(payload, plain)
|
||||
if uErr != nil {
|
||||
deps.log().Errorf("[STREAM %d] OBF unwrap failed: %v (n=%d)", streamID, uErr, n)
|
||||
continue
|
||||
}
|
||||
payload = plain[:m]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package srtpmimicry
|
||||
package rtpopus
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -30,7 +30,7 @@ func Listen(addr *net.UDPAddr, key []byte) (dtlsnet.PacketListener, error) {
|
||||
}
|
||||
inner, err := pionudp.Listen("udp", addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wrap: udp listen: %w", err)
|
||||
return nil, fmt.Errorf("rtpopus:udp listen: %w", err)
|
||||
}
|
||||
return &packetListener{
|
||||
inner: dtlsnet.PacketListenerFromListener(inner),
|
||||
@@ -81,7 +81,7 @@ func (c *packetConn) ReadFrom(p []byte) (int, net.Addr, error) {
|
||||
}
|
||||
wire := buf[:n]
|
||||
if len(wire) < Overhead {
|
||||
return 0, addr, errors.New("wrap: packet too short")
|
||||
return 0, addr, errors.New("rtpopus:packet too short")
|
||||
}
|
||||
m, err := c.conn.Unwrap(wire, p)
|
||||
if err != nil {
|
||||
@@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package srtpmimicry
|
||||
package rtpopus
|
||||
|
||||
import (
|
||||
"net"
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
// RelayPacketConn оборачивает TURN relay PacketConn, направляя все записи
|
||||
// фиксированному пиру. Если Conn non-nil, пакеты оборачиваются/разворачиваются
|
||||
// SRTP-mimicry AEAD.
|
||||
// rtpopus AEAD (мимикрия под RTP/opus).
|
||||
type RelayPacketConn struct {
|
||||
Relay net.PacketConn
|
||||
Peer net.Addr
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package srtpmimicry реализует AEAD-фрейминг с мимикрией SRTP для обхода
|
||||
// Package rtpopus реализует AEAD-фрейминг с мимикрией под RTP/opus (один
|
||||
// из планируемых wire-профилей обфускации в internal/wire/). Цель — обход
|
||||
// VK TURN content-filter.
|
||||
//
|
||||
// Назначение: обфускация, а не безопасность. DTLS уже обеспечивает
|
||||
@@ -25,7 +26,7 @@
|
||||
// AAD = первые 24 байта (RTP header || nonce).
|
||||
//
|
||||
// Wire-формат заморожен — требуется побитовая совместимость с задеплоенными пирами.
|
||||
package srtpmimicry
|
||||
package rtpopus
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
@@ -61,11 +62,11 @@ type State struct {
|
||||
|
||||
func NewState(key []byte) (*State, error) {
|
||||
if len(key) != KeyLen {
|
||||
return nil, fmt.Errorf("wrap: key must be %d bytes (got %d)", KeyLen, len(key))
|
||||
return nil, fmt.Errorf("rtpopus:key must be %d bytes (got %d)", KeyLen, len(key))
|
||||
}
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wrap: aead init: %w", err)
|
||||
return nil, fmt.Errorf("rtpopus:aead init: %w", err)
|
||||
}
|
||||
return &State{aead: aead}, nil
|
||||
}
|
||||
@@ -93,13 +94,13 @@ func NewConn(key []byte, isServer bool) (*Conn, error) {
|
||||
// переиспользуя переданный State.
|
||||
func NewConnFromState(state *State, isServer bool) (*Conn, error) {
|
||||
if state == nil {
|
||||
return nil, errors.New("wrap: nil state")
|
||||
return nil, errors.New("rtpopus:nil state")
|
||||
}
|
||||
c := &Conn{state: state}
|
||||
|
||||
var rnd [16]byte
|
||||
if _, err := rand.Read(rnd[:]); err != nil {
|
||||
return nil, fmt.Errorf("wrap: rand init: %w", err)
|
||||
return nil, fmt.Errorf("rtpopus:rand init: %w", err)
|
||||
}
|
||||
copy(c.sessionID[:], rnd[0:4])
|
||||
copy(c.ssrc[:], rnd[4:8])
|
||||
@@ -115,7 +116,7 @@ func NewConnFromState(state *State, isServer bool) (*Conn, error) {
|
||||
|
||||
var cb [8]byte
|
||||
if _, err := rand.Read(cb[:]); err != nil {
|
||||
return nil, fmt.Errorf("wrap: counter rand: %w", err)
|
||||
return nil, fmt.Errorf("rtpopus:counter rand: %w", err)
|
||||
}
|
||||
c.counter.Store(binary.BigEndian.Uint64(cb[:]))
|
||||
return c, nil
|
||||
@@ -126,7 +127,7 @@ func NewConnFromState(state *State, isServer bool) (*Conn, error) {
|
||||
func (c *Conn) WrapInto(dst, payload []byte) (int, error) {
|
||||
wireLen := Overhead + len(payload)
|
||||
if len(dst) < wireLen {
|
||||
return 0, errors.New("wrap: dst buffer too small")
|
||||
return 0, errors.New("rtpopus:dst buffer too small")
|
||||
}
|
||||
|
||||
// RTP-заголовок.
|
||||
@@ -158,7 +159,7 @@ func (c *Conn) WrapInto(dst, payload []byte) (int, error) {
|
||||
// потреблённым после вызова (содержимое нельзя переиспользовать).
|
||||
func (c *Conn) Unwrap(wire, dst []byte) (int, error) {
|
||||
if len(wire) < Overhead {
|
||||
return 0, errors.New("wrap: packet too short")
|
||||
return 0, errors.New("rtpopus:packet too short")
|
||||
}
|
||||
nonce := wire[rtpHdrLen : rtpHdrLen+nonceLen]
|
||||
aad := wire[:headerLen]
|
||||
@@ -166,10 +167,10 @@ func (c *Conn) Unwrap(wire, dst []byte) (int, error) {
|
||||
|
||||
plain, err := c.state.aead.Open(ct[:0], nonce, ct, aad)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("wrap: AEAD open: %w", err)
|
||||
return 0, fmt.Errorf("rtpopus:AEAD open: %w", err)
|
||||
}
|
||||
if len(plain) > len(dst) {
|
||||
return 0, errors.New("wrap: dst buffer too small")
|
||||
return 0, errors.New("rtpopus:dst buffer too small")
|
||||
}
|
||||
copy(dst[:len(plain)], plain)
|
||||
return len(plain), nil
|
||||
@@ -178,7 +179,7 @@ func (c *Conn) Unwrap(wire, dst []byte) (int, error) {
|
||||
func GenKeyHex() (string, error) {
|
||||
key := make([]byte, KeyLen)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
return "", fmt.Errorf("wrap: key gen: %w", err)
|
||||
return "", fmt.Errorf("rtpopus:key gen: %w", err)
|
||||
}
|
||||
return hex.EncodeToString(key), nil
|
||||
}
|
||||
@@ -190,7 +191,7 @@ func DecodeKey(enabled bool, raw string) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
if raw == "" {
|
||||
return nil, errors.New("-obf requires -obf-key")
|
||||
return nil, errors.New("-obf-profile != none requires -obf-key")
|
||||
}
|
||||
key, err := hex.DecodeString(raw)
|
||||
if err != nil {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package srtpmimicry
|
||||
package rtpopus
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
Reference in New Issue
Block a user