/* SPDX-License-Identifier: MIT
*
* Phobos
*/
package phobos
import (
"context"
"errors"
"fmt"
"net"
"net/netip"
"sync"
"sync/atomic"
"syscall"
"time"
)
type SocketControl func(network, address string, c syscall.RawConn) error
type UDPProxyConfig struct {
Target netip.AddrPort
Key []byte
Masking Masking
Media MediaParams
MaxDummy int
ObfuscateBytes int
UpstreamControl SocketControl
Logf func(format string, args ...any)
}
type UDPProxy struct {
config UDPProxyConfig
listener *net.UDPConn
upstream *net.UDPConn
listenPort uint16
maskerMu sync.Mutex
masker Masker
client atomic.Pointer[netip.AddrPort]
running atomic.Bool
sawTunnel atomic.Bool
sawServer atomic.Bool
sawRejected atomic.Bool
wait sync.WaitGroup
done chan struct{}
}
func NewUDPProxy(config UDPProxyConfig) *UDPProxy {
if config.Logf == nil {
config.Logf = func(string, ...any) {}
}
return &UDPProxy{config: config, done: make(chan struct{})}
}
func (p *UDPProxy) ListenPort() uint16 {
return p.listenPort
}
func (p *UDPProxy) Start() error {
if len(p.config.Key) == 0 {
return errors.New("obfuscation key is empty")
}
if !p.config.Target.IsValid() {
return errors.New("obfuscator target is not resolved")
}
listener, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
if err != nil {
return fmt.Errorf("unable to open loopback socket: %w", err)
}
dialer := net.Dialer{Control: p.config.UpstreamControl}
conn, err := dialer.DialContext(context.Background(), "udp", p.config.Target.String())
if err != nil {
listener.Close()
return fmt.Errorf("unable to open upstream socket: %w", err)
}
p.listener = listener
p.upstream = conn.(*net.UDPConn)
p.listenPort = uint16(listener.LocalAddr().(*net.UDPAddr).Port)
p.masker = NewMasker(p.config.Masking, p.config.Media)
p.running.Store(true)
p.spawn(p.clientLoop)
p.spawn(p.serverLoop)
if p.masker != nil {
p.spawn(p.timerLoop)
}
p.config.Logf("Obfuscator started: 127.0.0.1:%d -> %v (masking %v)", p.listenPort, p.config.Target, p.config.Masking)
return nil
}
func (p *UDPProxy) Stop() {
if !p.running.Swap(false) {
return
}
close(p.done)
p.listener.Close()
p.upstream.Close()
p.wait.Wait()
p.config.Logf("Obfuscator stopped: 127.0.0.1:%d -> %v", p.listenPort, p.config.Target)
}
func (p *UDPProxy) spawn(loop func()) {
p.wait.Add(1)
go func() {
defer p.wait.Done()
loop()
}()
}
func (p *UDPProxy) sendToServer(packet []byte) (int, error) {
return p.upstream.Write(packet)
}
func (p *UDPProxy) sendToClient(packet []byte) (int, error) {
client := p.client.Load()
if client == nil {
return 0, nil
}
return p.listener.WriteToUDPAddrPort(packet, *client)
}
func (p *UDPProxy) reject(stage string, length int) {
if !p.sawRejected.Swap(true) {
p.config.Logf("Obfuscator: server packet of %d bytes rejected at %s stage, check that masking and key match the server preset", length, stage)
}
}
func (p *UDPProxy) fail(what string, err error) {
if p.running.Load() {
p.config.Logf("Obfuscator %s failed: %v", what, err)
}
}
func (p *UDPProxy) clientLoop() {
buf := make([]byte, BufferSize)
obfuscator := NewObfuscator(p.config.Key)
for {
n, source, err := p.listener.ReadFromUDPAddrPort(buf)
if err != nil {
p.fail("loopback read", err)
return
}
if n < 4 {
continue
}
packetType := PacketType(buf)
if !IsKnownPacketType(packetType) {
continue
}
if client := p.client.Load(); client == nil || *client != source {
p.client.Store(&source)
}
if !p.sawTunnel.Swap(true) {
p.config.Logf("Obfuscator: first packet from tunnel, %d bytes from %v", n, source)
}
length := obfuscator.Encode(buf, n, p.config.MaxDummy, p.config.ObfuscateBytes)
if length < 0 {
continue
}
if p.masker != nil {
length = p.wrap(buf, length, packetType == TypeHandshake)
if length <= 0 {
continue
}
}
if _, err := p.sendToServer(buf[:length]); err != nil {
p.fail("upstream write", err)
return
}
}
}
func (p *UDPProxy) serverLoop() {
buf := make([]byte, BufferSize)
obfuscator := NewObfuscator(p.config.Key)
for {
n, err := p.upstream.Read(buf)
if err != nil {
p.fail("upstream read", err)
return
}
if !p.sawServer.Swap(true) {
p.config.Logf("Obfuscator: first packet from server, %d bytes", n)
}
if p.client.Load() == nil {
continue
}
length := n
if p.masker != nil {
length = p.unwrap(buf, length)
if length < 0 {
p.reject("masking", n)
continue
}
if length == 0 {
continue
}
}
if length < 4 {
p.reject("length", n)
continue
}
length = obfuscator.Decode(buf, length, p.config.ObfuscateBytes)
if length < 4 || !IsKnownPacketType(PacketType(buf)) {
p.reject("key", n)
continue
}
if _, err := p.sendToClient(buf[:length]); err != nil {
p.fail("loopback write", err)
return
}
}
}
func (p *UDPProxy) timerLoop() {
ticker := time.NewTicker(p.masker.TimerInterval())
defer ticker.Stop()
for {
select {
case <-p.done:
return
case <-ticker.C:
if p.client.Load() == nil {
continue
}
p.maskerMu.Lock()
p.masker.OnTimer(p.sendToServer)
p.maskerMu.Unlock()
}
}
}
func (p *UDPProxy) wrap(buf []byte, length int, handshake bool) int {
p.maskerMu.Lock()
defer p.maskerMu.Unlock()
if handshake {
p.masker.OnHandshakeRequest(p.sendToServer)
}
return p.masker.OnDataWrap(buf, length)
}
func (p *UDPProxy) unwrap(buf []byte, length int) int {
p.maskerMu.Lock()
defer p.maskerMu.Unlock()
return p.masker.OnDataUnwrap(buf, length, p.config.Target, p.sendToServer)
}