Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion common/version.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package common

// Must be a valid semver
const Version = "v2.1.6"
const Version = "v2.1.7"
61 changes: 61 additions & 0 deletions egress/cmd/sing-box/egress_sing_box.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package main

import (
"context"
"fmt"
"net"
"os"

"github.com/armon/go-socks5"

"github.com/getlantern/broflake/common"
"github.com/getlantern/broflake/egress"
egcmdcommon "github.com/getlantern/broflake/egress/cmd/common"
)

func main() {
ctx := context.Background()
port := os.Getenv("PORT")
if port == "" {
port = "8000"
}

l, err := net.Listen("tcp", fmt.Sprintf(":%v", port))
if err != nil {
panic(err)
}

common.Debugf("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
common.Debugf("@ DANGER @")
common.Debugf("@ DANGER @")
common.Debugf("@ DANGER @")
common.Debugf("@ @")
common.Debugf("@ This standalone egress server does not use secure TLS @")
common.Debugf("@ at the QUIC layer! @")
common.Debugf("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n")

// And here's why it doesn't use secure TLS at the QUIC layer
tlsConfig := egcmdcommon.GenerateSelfSignedTLSConfig(true)

ll, err := egress.NewListener(ctx, l, tlsConfig)
if err != nil {
panic(err)
}
defer ll.Close()

conf := &socks5.Config{
Dial: UoTDialer(),
Resolver: &UoTResolver{},
}
proxy, err := socks5.New(conf)
if err != nil {
panic(err)
}

common.Debugf("Starting SOCKS5 UoT proxy...")

err = proxy.Serve(ll)
if err != nil {
panic(err)
}
}
279 changes: 279 additions & 0 deletions egress/cmd/sing-box/uot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
package main

import (
"context"
"encoding/binary"
"fmt"
"io"
"net"
"strings"
"time"

"github.com/getlantern/broflake/common"
)

const (
uotMagicAddress = "sp.v2.udp-over-tcp.arpa"
uotLegacyMagicAddress = "sp.udp-over-tcp.arpa"

// SOCKS5 address types (used in UoT v2 request header via SocksaddrSerializer)
socksAddrTypeIPv4 = 0x01
socksAddrTypeFQDN = 0x03
socksAddrTypeIPv6 = 0x04

maxUDPPayload = 65507
)

// UoTResolver is a SOCKS5 DNS resolver that passes UoT magic addresses through
// without resolution, falling back to standard DNS for everything else.
// go-socks5 resolves FQDNs before calling Dial, so we must intercept here.
type UoTResolver struct{}

func (r *UoTResolver) Resolve(ctx context.Context, name string) (context.Context, net.IP, error) {
if isUoTMagicName(name) {
// Return nil IP so AddrSpec.Address() falls through to the FQDN,
// allowing the Dial function to match on the magic address.
return ctx, nil, nil
}
ips, err := net.DefaultResolver.LookupIPAddr(ctx, name)
if err != nil {
return ctx, nil, err
}
if len(ips) == 0 {
return ctx, nil, fmt.Errorf("no IP addresses found for %s", name)
}
return ctx, ips[0].IP, nil
}

func isUoTMagicName(name string) bool {
name = strings.TrimSuffix(name, ".")
return strings.EqualFold(name, uotMagicAddress) || strings.EqualFold(name, uotLegacyMagicAddress)
}

func isUoTAddress(addr string) bool {
host := addr
if h, _, err := net.SplitHostPort(addr); err == nil {
host = h
}
return isUoTMagicName(host)
}

// tcpPipeConn wraps a net.Conn (from net.Pipe) so that LocalAddr() returns a
// *net.TCPAddr. go-socks5 does target.LocalAddr().(*net.TCPAddr) after Dial.
type tcpPipeConn struct {
net.Conn
}

func (c *tcpPipeConn) LocalAddr() net.Addr {
return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0}
}

func (c *tcpPipeConn) RemoteAddr() net.Addr {
return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0}
}

type uotRequest struct {
IsConnect bool
Destination *net.UDPAddr
}

// readUoTRequest reads a UoT v2 request header.
// Wire format: 1 byte IsConnect (bool) + SOCKS5-style address (ATYP + addr + port).
func readUoTRequest(r io.Reader) (*uotRequest, error) {
var isConnect byte
if err := binary.Read(r, binary.BigEndian, &isConnect); err != nil {
return nil, fmt.Errorf("read IsConnect: %w", err)
}

addr, err := readSocksAddr(r)
if err != nil {
return nil, fmt.Errorf("read destination: %w", err)
}

return &uotRequest{
IsConnect: isConnect != 0,
Destination: addr,
}, nil
}

// readSocksAddr reads a SOCKS5-style address: ATYP + address + 2-byte port (big-endian).
func readSocksAddr(r io.Reader) (*net.UDPAddr, error) {
var atyp byte
if err := binary.Read(r, binary.BigEndian, &atyp); err != nil {
return nil, err
}

var ip net.IP
var host string

switch atyp {
case socksAddrTypeIPv4:
ip = make(net.IP, 4)
if _, err := io.ReadFull(r, ip); err != nil {
return nil, err
}
case socksAddrTypeIPv6:
ip = make(net.IP, 16)
if _, err := io.ReadFull(r, ip); err != nil {
return nil, err
}
case socksAddrTypeFQDN:
var length byte
if err := binary.Read(r, binary.BigEndian, &length); err != nil {
return nil, err
}
domain := make([]byte, length)
if _, err := io.ReadFull(r, domain); err != nil {
return nil, err
}
host = string(domain)
default:
return nil, fmt.Errorf("unknown address type: %d", atyp)
}

var port uint16
if err := binary.Read(r, binary.BigEndian, &port); err != nil {
return nil, err
}

if host != "" {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)
if err != nil {
return nil, fmt.Errorf("resolve %s: %w", host, err)
}
if len(ips) == 0 {
return nil, fmt.Errorf("no IP addresses found for %s", host)
}
return &net.UDPAddr{IP: ips[0].IP, Port: int(port)}, nil
}

return &net.UDPAddr{IP: ip, Port: int(port)}, nil
}

// handleUoT handles a UoT v2 connection by relaying framed UDP packets between
// a TCP stream and a real UDP socket.
func handleUoT(tcpConn net.Conn) {
defer tcpConn.Close()

req, err := readUoTRequest(tcpConn)
if err != nil {
common.Debugf("UoT: failed to read request: %v", err)
return
}

common.Debugf("UoT: relay to %v (isConnect=%v)", req.Destination, req.IsConnect)

if !req.IsConnect {
common.Debugf("UoT: non-connect mode not supported")
return
}

if req.Destination.IP.IsLoopback() {
common.Debugf("UoT: refusing to relay to loopback address %v", req.Destination)
return
}

udpConn, err := net.DialUDP("udp", nil, req.Destination)
if err != nil {
common.Debugf("UoT: failed to dial UDP %v: %v", req.Destination, err)
return
}
defer udpConn.Close()

common.Debugf("UoT: connected UDP to %v", req.Destination)

done := make(chan struct{}, 2)

// When one direction exits, close both connections to unblock the other goroutine.
cleanup := func() {
tcpConn.Close()
udpConn.Close()
done <- struct{}{}
}

// TCP → UDP: read length-prefixed packets from TCP, send as raw UDP
go func() {
defer cleanup()
buf := make([]byte, maxUDPPayload)
for {
var length uint16
if err := binary.Read(tcpConn, binary.BigEndian, &length); err != nil {
common.Debugf("UoT: TCP read length error: %v", err)
return
}
if int(length) > maxUDPPayload {
common.Debugf("UoT: dropping oversized frame (%d bytes, max %d)", length, maxUDPPayload)
if _, err := io.CopyN(io.Discard, tcpConn, int64(length)); err != nil {
common.Debugf("UoT: TCP discard error: %v", err)
return
}
continue
}
if _, err := io.ReadFull(tcpConn, buf[:length]); err != nil {
common.Debugf("UoT: TCP read payload error: %v", err)
return
}
if _, err := udpConn.Write(buf[:length]); err != nil {
common.Debugf("UoT: UDP write error: %v", err)
return
}
}
}()

// UDP → TCP: read raw UDP, write as length-prefixed packets to TCP
go func() {
defer cleanup()
buf := make([]byte, maxUDPPayload)
for {
n, err := udpConn.Read(buf)
if err != nil {
common.Debugf("UoT: UDP read error: %v", err)
return
}
if n > maxUDPPayload {
common.Debugf("UoT: dropping oversized UDP packet (%d bytes)", n)
continue
}
if err := binary.Write(tcpConn, binary.BigEndian, uint16(n)); err != nil {
common.Debugf("UoT: TCP write length error: %v", err)
return
}
if _, err := tcpConn.Write(buf[:n]); err != nil {
common.Debugf("UoT: TCP write payload error: %v", err)
return
}
}
}()

<-done
}

// UoTDialer returns a SOCKS5-compatible dial function that intercepts UoT magic
// addresses and handles them as UDP-over-TCP tunnels, while dialing everything
// else normally.
func UoTDialer() func(ctx context.Context, network, addr string) (net.Conn, error) {
return func(ctx context.Context, network, addr string) (net.Conn, error) {
if isUoTAddress(addr) {
common.Debugf("UoT: intercepting %v", addr)
client, server := net.Pipe()
connDone := make(chan struct{})
go func() {
select {
case <-ctx.Done():
client.Close()
server.Close()
case <-connDone:
}
}()
go func() {
handleUoT(server)
close(connDone)
}()
return &tcpPipeConn{Conn: client}, nil
}
var d net.Dialer
return d.DialContext(ctx, network, addr)
}
}