Ground-Zerro / Phobos Public
Code Issues Pull requests Actions Releases View on GitHub ↗
4.4 KB go
/* SPDX-License-Identifier: MIT
 *
 * Phobos
 */

package phobos

import (
	"bytes"
	"net"
	"net/netip"
	"testing"
	"time"
)

type fakeServer struct {
	conn           *net.UDPConn
	key            []byte
	masking        Masking
	media          MediaParams
	obfuscateBytes int
}

func startFakeServer(t *testing.T, key []byte, masking Masking, media MediaParams, obfuscateBytes int) *fakeServer {
	t.Helper()
	conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
	if err != nil {
		t.Fatalf("unable to listen: %v", err)
	}
	server := &fakeServer{conn: conn, key: key, masking: masking, media: media, obfuscateBytes: obfuscateBytes}
	t.Cleanup(func() { conn.Close() })
	go server.run()
	return server
}

func (s *fakeServer) addr() netip.AddrPort {
	return s.conn.LocalAddr().(*net.UDPAddr).AddrPort()
}

func (s *fakeServer) run() {
	buf := make([]byte, BufferSize)
	obfuscator := NewObfuscator(s.key)
	masker := NewMasker(s.masking, s.media)
	for {
		n, source, err := s.conn.ReadFromUDPAddrPort(buf)
		if err != nil {
			return
		}
		send := func(p []byte) (int, error) { return s.conn.WriteToUDPAddrPort(p, source) }

		length := n
		if masker != nil {
			if length = masker.OnDataUnwrap(buf, length, source, send); length <= 0 {
				continue
			}
		}
		if length = obfuscator.Decode(buf, length, s.obfuscateBytes); length < 4 {
			continue
		}

		buf[0] = TypeHandshakeResponse
		length = obfuscator.Encode(buf, length, DefaultMaxDummy, s.obfuscateBytes)
		if masker != nil {
			if length = masker.OnDataWrap(buf, length); length <= 0 {
				continue
			}
		}
		send(buf[:length])
	}
}

func handshakePacket(length int) []byte {
	packet := make([]byte, length)
	for i := range packet {
		packet[i] = byte(i * 3)
	}
	packet[0], packet[1], packet[2], packet[3] = TypeHandshake, 0, 0, 0
	return packet
}

func TestUDPProxyRoundTrip(t *testing.T) {
	key := []byte("Ic0OGtSf1BdMmMDzs7GmYRuPS/HGmNXsSU9EOWEeuQI=")
	cases := []struct {
		name           string
		masking        Masking
		media          MediaParams
		obfuscateBytes int
	}{
		{"none", MaskingNone, MediaParams{}, 0},
		{"stun", MaskingSTUN, MediaParams{}, 0},
		{"media", MaskingMEDIA, MediaParams{PayloadType: 102, SSRC: 0xC0FFEE, TimestampStep: 3000}, MediaObfuscateBytesDefault},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			server := startFakeServer(t, key, tc.masking, tc.media, tc.obfuscateBytes)

			proxy := NewUDPProxy(UDPProxyConfig{
				Target:         server.addr(),
				Key:            key,
				Masking:        tc.masking,
				Media:          tc.media,
				MaxDummy:       DefaultMaxDummy,
				ObfuscateBytes: tc.obfuscateBytes,
				Logf:           t.Logf,
			})
			if err := proxy.Start(); err != nil {
				t.Fatalf("unable to start proxy: %v", err)
			}
			t.Cleanup(proxy.Stop)

			client, err := net.DialUDP("udp4", nil, &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: int(proxy.ListenPort())})
			if err != nil {
				t.Fatalf("unable to dial proxy: %v", err)
			}
			defer client.Close()

			for _, length := range []int{148, 92, 1420} {
				packet := handshakePacket(length)
				if _, err := client.Write(packet); err != nil {
					t.Fatalf("unable to send: %v", err)
				}

				client.SetReadDeadline(time.Now().Add(5 * time.Second))
				reply := make([]byte, BufferSize)
				n, err := client.Read(reply)
				if err != nil {
					t.Fatalf("no reply for length %d: %v", length, err)
				}
				if n != length {
					t.Fatalf("reply length %d, want %d", n, length)
				}
				expected := bytes.Clone(packet)
				expected[0] = TypeHandshakeResponse
				if !bytes.Equal(reply[:n], expected) {
					t.Fatalf("reply payload mismatch at length %d", length)
				}
			}
		})
	}
}

func TestUDPProxyRejectsEmptyKey(t *testing.T) {
	proxy := NewUDPProxy(UDPProxyConfig{Target: netip.MustParseAddrPort("127.0.0.1:1")})
	if err := proxy.Start(); err == nil {
		t.Fatal("expected an error for an empty key")
	}
}

func TestUDPProxyRejectsUnresolvedTarget(t *testing.T) {
	proxy := NewUDPProxy(UDPProxyConfig{Key: []byte("key")})
	if err := proxy.Start(); err == nil {
		t.Fatal("expected an error for an unresolved target")
	}
}

func TestUDPProxyStopIsIdempotent(t *testing.T) {
	server := startFakeServer(t, []byte("key"), MaskingNone, MediaParams{}, 0)
	proxy := NewUDPProxy(UDPProxyConfig{Target: server.addr(), Key: []byte("key"), Logf: t.Logf})
	if err := proxy.Start(); err != nil {
		t.Fatalf("unable to start proxy: %v", err)
	}
	proxy.Stop()
	proxy.Stop()
}