mirror of
https://github.com/hackdiaz-dev/free-turn-proxy.git
synced 2026-10-12 09:41:53 +02:00
418 lines
12 KiB
Go
418 lines
12 KiB
Go
package mobile
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"math"
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/samosvalishe/free-turn-proxy/internal/config"
|
|
"github.com/samosvalishe/free-turn-proxy/internal/logx"
|
|
"github.com/samosvalishe/free-turn-proxy/internal/netctl"
|
|
"github.com/samosvalishe/free-turn-proxy/internal/tunnel"
|
|
)
|
|
|
|
func wgKey(b byte) string {
|
|
raw := make([]byte, tunnel.KeyLen)
|
|
for i := range raw {
|
|
raw[i] = b
|
|
}
|
|
return base64.StdEncoding.EncodeToString(raw)
|
|
}
|
|
|
|
func wgConf() string {
|
|
return "[Interface]\nPrivateKey = " + wgKey(1) + "\nAddress = 10.8.0.2/32\n\n" +
|
|
"[Peer]\nPublicKey = " + wgKey(2) + "\nAllowedIPs = 0.0.0.0/0\n" +
|
|
"Endpoint = 1.2.3.4:51820\n"
|
|
}
|
|
|
|
func awgConf() string {
|
|
return "[Interface]\nPrivateKey = " + wgKey(1) + "\nAddress = 10.8.0.2/32\n" +
|
|
"Jc = 4\nJmin = 40\nJmax = 70\nS1 = 15\n\n" +
|
|
"[Peer]\nPublicKey = " + wgKey(2) + "\nAllowedIPs = 0.0.0.0/0\n"
|
|
}
|
|
|
|
func tunnelConfigJSON(t *testing.T, mode, conf string) string {
|
|
t.Helper()
|
|
payload := map[string]any{
|
|
"peer": "1.2.3.4:5000",
|
|
"clientId": "deadbeef",
|
|
"vk": map[string]any{"links": []string{"CODE"}},
|
|
"tunnel": map[string]any{"mode": mode, "config": conf},
|
|
}
|
|
b, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func parsedConfig(t *testing.T, jsonCfg string) *config.Client {
|
|
t.Helper()
|
|
cfg, err := config.ParseClientJSON([]byte(jsonCfg), "")
|
|
if err != nil {
|
|
t.Fatalf("ParseClientJSON() error = %v", err)
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
func TestBuildTunnelWiresPipeToBind(t *testing.T) {
|
|
cfg := parsedConfig(t, tunnelConfigJSON(t, "awg", awgConf()))
|
|
|
|
parts, tunCfg, err := buildTunnel(cfg, logx.Nop())
|
|
if err != nil {
|
|
t.Fatalf("buildTunnel() error = %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
parts.close()
|
|
_ = parts.relaySide.Close()
|
|
})
|
|
|
|
if parts.bind == nil || parts.backend == nil || parts.relaySide == nil {
|
|
t.Fatalf("parts = %+v", parts)
|
|
}
|
|
if !tunCfg.Amnezia.Enabled() {
|
|
t.Error("awg params dropped in mode=awg")
|
|
}
|
|
if tunCfg.MTU != tunnel.DefaultMTU {
|
|
t.Errorf("MTU = %d, want %d", tunCfg.MTU, tunnel.DefaultMTU)
|
|
}
|
|
if tunCfg.Peers[0].Endpoint != "" {
|
|
t.Errorf("Endpoint = %q, want empty", tunCfg.Peers[0].Endpoint)
|
|
}
|
|
}
|
|
|
|
// Проверка освобождения пайпов при закрытии TunnelParts.
|
|
func TestTunnelPartsCloseReleasesPipes(t *testing.T) {
|
|
cfg := parsedConfig(t, tunnelConfigJSON(t, "wg", wgConf()))
|
|
|
|
parts, _, err := buildTunnel(cfg, logx.Nop())
|
|
if err != nil {
|
|
t.Fatalf("buildTunnel() error = %v", err)
|
|
}
|
|
|
|
parts.close()
|
|
parts.close()
|
|
|
|
if _, err := parts.relaySide.WriteTo([]byte{1}, nil); !errors.Is(err, net.ErrClosed) {
|
|
t.Errorf("relaySide.WriteTo() error = %v, want %v", err, net.ErrClosed)
|
|
}
|
|
if _, err := parts.deviceSide.WriteTo([]byte{1}, nil); !errors.Is(err, net.ErrClosed) {
|
|
t.Errorf("deviceSide.WriteTo() error = %v, want %v", err, net.ErrClosed)
|
|
}
|
|
}
|
|
|
|
// Режим wg отключает параметры AmneziaWG.
|
|
func TestBuildTunnelStripsAmneziaInWGMode(t *testing.T) {
|
|
cfg := parsedConfig(t, tunnelConfigJSON(t, "wg", awgConf()))
|
|
|
|
parts, tunCfg, err := buildTunnel(cfg, logx.Nop())
|
|
if err != nil {
|
|
t.Fatalf("buildTunnel() error = %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
parts.close()
|
|
_ = parts.relaySide.Close()
|
|
})
|
|
|
|
if tunCfg.Amnezia.Enabled() {
|
|
t.Errorf("amnezia params kept in mode=wg: %+v", tunCfg.Amnezia)
|
|
}
|
|
}
|
|
|
|
func TestBuildTunnelRejectsBrokenConfig(t *testing.T) {
|
|
cfg := parsedConfig(t, tunnelConfigJSON(t, "wg", wgConf()))
|
|
cfg.Tunnel.Config = "[Interface]\nPrivateKey = nonsense\n"
|
|
|
|
if _, _, err := buildTunnel(cfg, logx.Nop()); err == nil {
|
|
t.Fatal("buildTunnel() error = nil for broken config")
|
|
}
|
|
}
|
|
|
|
func TestStartRejectsTunnelMode(t *testing.T) {
|
|
t.Cleanup(Stop)
|
|
err := Start(tunnelConfigJSON(t, "awg", awgConf()))
|
|
if !errors.Is(err, ErrTunnelRequiresStartTunnel) {
|
|
t.Fatalf("Start() error = %v, want ErrTunnelRequiresStartTunnel", err)
|
|
}
|
|
}
|
|
|
|
func TestStartTunnelRejectsBadFD(t *testing.T) {
|
|
t.Cleanup(Stop)
|
|
if err := StartTunnel(tunnelConfigJSON(t, "wg", wgConf()), -1); err == nil {
|
|
t.Fatal("StartTunnel() error = nil for negative fd")
|
|
}
|
|
}
|
|
|
|
func TestValidateConfigRejectsTunnelWithoutConfig(t *testing.T) {
|
|
msg := ValidateConfig(tunnelConfigJSON(t, "wg", ""))
|
|
if !strings.Contains(msg, "tunnel config") {
|
|
t.Fatalf("ValidateConfig() = %q, want missing-config error", msg)
|
|
}
|
|
}
|
|
|
|
func TestValidateConfigRejectsUnknownTunnelMode(t *testing.T) {
|
|
msg := ValidateConfig(tunnelConfigJSON(t, "openvpn", wgConf()))
|
|
if !strings.Contains(msg, "invalid tunnel mode") {
|
|
t.Fatalf("ValidateConfig() = %q, want invalid-mode error", msg)
|
|
}
|
|
}
|
|
|
|
func TestParseTunnelConfig(t *testing.T) {
|
|
conf := "[Interface]\nPrivateKey = " + wgKey(1) + "\nAddress = 10.8.0.2/32, fd00::2/128\n" +
|
|
"DNS = 1.1.1.1, 8.8.8.8\nMTU = 1420\n\n" +
|
|
"[Peer]\nPublicKey = " + wgKey(2) + "\nAllowedIPs = 0.0.0.0/0\n\n" +
|
|
"[Peer]\nPublicKey = " + wgKey(3) + "\nAllowedIPs = 0.0.0.0/0, 10.9.0.0/24\n"
|
|
|
|
p, err := ParseTunnelConfig(conf, 0)
|
|
if err != nil {
|
|
t.Fatalf("ParseTunnelConfig() error = %v", err)
|
|
}
|
|
if p.Addresses != "10.8.0.2/32,fd00::2/128" {
|
|
t.Errorf("Addresses = %q", p.Addresses)
|
|
}
|
|
if p.DNS != "1.1.1.1,8.8.8.8" {
|
|
t.Errorf("DNS = %q", p.DNS)
|
|
}
|
|
if p.AllowedIPs != "0.0.0.0/0,10.9.0.0/24" {
|
|
t.Errorf("AllowedIPs = %q", p.AllowedIPs)
|
|
}
|
|
if p.MTU != 1420 {
|
|
t.Errorf("MTU = %d, want 1420", p.MTU)
|
|
}
|
|
}
|
|
|
|
func TestParseTunnelConfigMTUOverride(t *testing.T) {
|
|
p, err := ParseTunnelConfig(wgConf(), 1280)
|
|
if err != nil {
|
|
t.Fatalf("ParseTunnelConfig() error = %v", err)
|
|
}
|
|
if p.MTU != 1280 {
|
|
t.Errorf("MTU = %d, want 1280", p.MTU)
|
|
}
|
|
}
|
|
|
|
func TestParseTunnelConfigRejectsBroken(t *testing.T) {
|
|
if _, err := ParseTunnelConfig("[Interface]\nPrivateKey = nonsense\n", 0); err == nil {
|
|
t.Fatal("ParseTunnelConfig() error = nil for broken config")
|
|
}
|
|
}
|
|
|
|
func TestTunnelStatsWithoutTunnel(t *testing.T) {
|
|
Stop()
|
|
st := TunnelStats()
|
|
if st.Up || st.HandshakeAgeSec != -1 {
|
|
t.Fatalf("TunnelStats() = %+v, want down", st)
|
|
}
|
|
}
|
|
|
|
// Проверка насыщения при math.MaxUint64 для gomobile int64.
|
|
func TestClampToInt64Overflow(t *testing.T) {
|
|
if got := clampToInt64(math.MaxUint64); got != math.MaxInt64 {
|
|
t.Errorf("clampToInt64(MaxUint64) = %d, want MaxInt64 (%d)", got, int64(math.MaxInt64))
|
|
}
|
|
if got := clampToInt64(0); got != 0 {
|
|
t.Errorf("clampToInt64(0) = %d, want 0", got)
|
|
}
|
|
if got := clampToInt64(uint64(math.MaxInt64)); got != math.MaxInt64 {
|
|
t.Errorf("clampToInt64(MaxInt64) = %d, want MaxInt64", got)
|
|
}
|
|
}
|
|
|
|
// В tcp-режиме ядро tun не читает: принятый fd остался бы установленным вхолостую.
|
|
func TestStartTunnelRejectsTCPMode(t *testing.T) {
|
|
const cfg = `{"peer":"1.2.3.4:5000","clientId":"deadbeef","proxy":{"mode":"tcp"},"vk":{"links":["https://vk.ru/call/join/CODE"]}}`
|
|
if err := StartTunnel(cfg, 7); !errors.Is(err, ErrTCPModeRequiresStart) {
|
|
t.Fatalf("StartTunnel() error = %v, want ErrTCPModeRequiresStart", err)
|
|
}
|
|
}
|
|
|
|
func TestDirectConfigRequiresEndpoint(t *testing.T) {
|
|
noEndpoint := awgConf()
|
|
hostname := strings.Replace(wgConf(), "1.2.3.4:51820", "vpn.example.com:51820", 1)
|
|
for _, conf := range []string{noEndpoint, hostname} {
|
|
if _, err := directConfig(conf, 0); err == nil {
|
|
t.Fatal("directConfig() error = nil, want ip:port endpoint")
|
|
}
|
|
}
|
|
cfg, err := directConfig(wgConf(), 1376)
|
|
if err != nil {
|
|
t.Fatalf("directConfig() error = %v", err)
|
|
}
|
|
if cfg.MTU != 1376 {
|
|
t.Errorf("MTU = %d, want 1376", cfg.MTU)
|
|
}
|
|
if cfg.Peers[0].Endpoint != "1.2.3.4:51820" {
|
|
t.Errorf("Endpoint = %q, want kept", cfg.Peers[0].Endpoint)
|
|
}
|
|
}
|
|
|
|
func TestStartDirectTunnelRejectsBadFD(t *testing.T) {
|
|
if err := StartDirectTunnel(wgConf(), 0, 0); err == nil {
|
|
t.Fatal("StartDirectTunnel() error = nil for fd 0")
|
|
}
|
|
}
|
|
|
|
// stubBackend подменяет устройство: реальный tun в тестах не поднять.
|
|
type stubBackend struct {
|
|
stats tunnel.Stats
|
|
err error
|
|
}
|
|
|
|
func (stubBackend) Up(*tunnel.Config, int) error { return nil }
|
|
func (stubBackend) Down() error { return nil }
|
|
func (s stubBackend) Stats() (tunnel.Stats, error) { return s.stats, s.err }
|
|
|
|
func TestDirectSnapshotPhase(t *testing.T) {
|
|
stale := time.Now().Add(-2 * directHandshakeStale)
|
|
cases := []struct {
|
|
name string
|
|
backend stubBackend
|
|
prep func(*live)
|
|
want string
|
|
}{
|
|
{"no handshake", stubBackend{}, nil, StateConnecting},
|
|
{"fresh handshake", stubBackend{stats: tunnel.Stats{LastHandshake: time.Now()}}, nil, StateConnected},
|
|
{"stale handshake idle", stubBackend{stats: tunnel.Stats{LastHandshake: stale}}, nil, StateConnected},
|
|
{
|
|
"stale handshake with tx",
|
|
stubBackend{stats: tunnel.Stats{LastHandshake: stale, TxBytes: 2000}},
|
|
func(l *live) { l.prevAt = time.Now().Add(-time.Second) },
|
|
StateConnecting,
|
|
},
|
|
{"stats error", stubBackend{err: errors.New("boom")}, nil, StateError},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
l := &live{tunnel: &tunnelParts{backend: tc.backend}}
|
|
if tc.prep != nil {
|
|
tc.prep(l)
|
|
}
|
|
if got := directSnapshot(l).State; got != tc.want {
|
|
t.Errorf("State = %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDirectSnapshotErrorKeepsStreams(t *testing.T) {
|
|
l := &live{tunnel: &tunnelParts{backend: stubBackend{err: errors.New("boom")}}}
|
|
|
|
s := directSnapshot(l)
|
|
|
|
if s.Streams != 1 || s.Total != 1 {
|
|
t.Errorf("streams = %d/%d, want 1/1", s.Streams, s.Total)
|
|
}
|
|
}
|
|
|
|
// Скорость берётся из дельты между опросами хоста.
|
|
func TestFillRatesUsesDelta(t *testing.T) {
|
|
now := time.Now()
|
|
l := &live{prevTx: 1000, prevRx: 500, prevAt: now.Add(-2 * time.Second)}
|
|
s := &Snapshot{TxTotal: 3000, RxTotal: 1500}
|
|
|
|
l.fillRatesAt(s, now)
|
|
|
|
if s.TxRate != 1000 || s.RxRate != 500 {
|
|
t.Errorf("rates = %d/%d, want 1000/500", s.TxRate, s.RxRate)
|
|
}
|
|
if l.prevTx != 3000 || l.prevRx != 1500 {
|
|
t.Errorf("prev = %d/%d, want 3000/1500", l.prevTx, l.prevRx)
|
|
}
|
|
}
|
|
|
|
// Первый опрос не с чем сравнивать - скорость нулевая, а не мусорная.
|
|
func TestFillRatesFirstCall(t *testing.T) {
|
|
l := &live{}
|
|
s := &Snapshot{TxTotal: 3000, RxTotal: 1500}
|
|
|
|
l.fillRates(s)
|
|
|
|
if s.TxRate != 0 || s.RxRate != 0 {
|
|
t.Errorf("rates = %d/%d, want 0/0", s.TxRate, s.RxRate)
|
|
}
|
|
}
|
|
|
|
type testProtector struct {
|
|
protected []int
|
|
ok bool
|
|
}
|
|
|
|
func (p *testProtector) Protect(fd int) bool {
|
|
p.protected = append(p.protected, fd)
|
|
return p.ok
|
|
}
|
|
|
|
func TestProtectFDReturnsStatus(t *testing.T) {
|
|
SetProtect(nil)
|
|
if protectFD(42) {
|
|
t.Error("protectFD() = true when no protector set")
|
|
}
|
|
|
|
p := &testProtector{ok: true}
|
|
SetProtect(p)
|
|
t.Cleanup(func() { SetProtect(nil) })
|
|
|
|
if !protectFD(42) {
|
|
t.Error("protectFD(42) = false, want true")
|
|
}
|
|
if len(p.protected) != 1 || p.protected[0] != 42 {
|
|
t.Errorf("protected = %v, want [42]", p.protected)
|
|
}
|
|
|
|
p.ok = false
|
|
if protectFD(43) {
|
|
t.Error("protectFD(43) = true, want false when protect fails")
|
|
}
|
|
}
|
|
|
|
func TestSetProtectFailsDial(t *testing.T) {
|
|
p := &testProtector{}
|
|
SetProtect(p)
|
|
t.Cleanup(func() { SetProtect(nil) })
|
|
|
|
d := net.Dialer{Control: netctl.Apply}
|
|
conn, err := d.Dial("udp", "127.0.0.1:9")
|
|
if err == nil {
|
|
_ = conn.Close()
|
|
}
|
|
if !errors.Is(err, ErrProtect) {
|
|
t.Fatalf("Dial = %v, want ErrProtect", err)
|
|
}
|
|
|
|
p.ok = true
|
|
conn, err = d.Dial("udp", "127.0.0.1:9")
|
|
if err != nil {
|
|
t.Fatalf("Dial with protect ok = %v", err)
|
|
}
|
|
_ = conn.Close()
|
|
}
|
|
|
|
type stubRebinder struct {
|
|
stubBackend
|
|
rebound int
|
|
}
|
|
|
|
func (s *stubRebinder) Rebind() error {
|
|
s.rebound++
|
|
return nil
|
|
}
|
|
|
|
func TestDirectReconnectInvokesRebind(t *testing.T) {
|
|
reb := &stubRebinder{}
|
|
current.Store(&live{
|
|
tunnel: &tunnelParts{backend: reb},
|
|
})
|
|
t.Cleanup(func() { current.Store(nil) })
|
|
|
|
Reconnect()
|
|
|
|
if reb.rebound != 1 {
|
|
t.Errorf("rebound = %d, want 1", reb.rebound)
|
|
}
|
|
}
|