Skip to content
Open
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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
//to forward http3 udp:
TS_DEBUG_MTU=1400 ./ts-proxy -udp <relay ts ip>:8443=<target ts ip>:8443


# ts-proxy
`ts-proxy` is a userspace Tailscale client that provides TCP/UDP port-forwarding and SOCKS5 proxy with UDP support. Full cone NAT and no UDP-over-TCP for SOCKS5 UDP (as long as Tailscale is not falling back on DERP).

Expand Down Expand Up @@ -110,4 +114,4 @@ The latest Tailscale release has a bug ( https://github.com/tailscale/tailscale/

# TODO
- HTTP Proxy support
- SOCKS5 authentication
- SOCKS5 authentication
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func main() {
log.Printf("Invalid UDP rule format: %s", raw)
continue
}
go tsproxy.ForwardUDP(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]))
go tsproxy.ForwardUDPV3(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]))
}

for _, raw := range fSocksRaw {
Expand Down
Binary file added ts-proxy-fixed
Binary file not shown.
134 changes: 134 additions & 0 deletions tsproxy/port_forward_udp_fixed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package tsproxy

import (
"io"
"log"
"net"
"sync"
"time"
"tailscale.com/tsnet"
)

// ForwardUDP using Tailscale's Listen/Accept API (correct way)
func (t *TsProxy) ForwardUDPFixed(bind, connect string) error {
bind = resolveTshost(t.tsServer, t.tsServer.Hostname, bind)
connect = resolveTshost(t.tsServer, t.tsServer.Hostname, connect)

// Use Listen instead of ListenPacket!
ln, err := listenUDPAsListener(t.tsServer, bind)
if err != nil {
log.Printf("[UDP] Listen failed: %v", err)
return err
}
defer ln.Close()

if t.debug {
log.Printf("[UDP] Listening on %s, forwarding to %s", ln.Addr(), connect)
}

for {
// Accept each UDP "connection" (flow from a specific client)
conn, err := ln.Accept()
if err != nil {
log.Printf("[UDP] Accept error: %v", err)
return err
}

if t.debug {
log.Printf("[UDP] Accepted connection from %s", conn.RemoteAddr())
}

// Handle this UDP flow in a goroutine
go t.handleUDPConn(conn, connect)
}
}

func (t *TsProxy) handleUDPConn(clientConn net.Conn, connect string) {
defer clientConn.Close()

// Dial to destination
dstConn, err := dialAny(t.tsServer, "udp", connect)
if err != nil {
if t.debug {
log.Printf("[UDP] Dial failed (%s): %v", connect, err)
}
return
}
defer dstConn.Close()

if t.debug {
log.Printf("[UDP] Connected: %s <-> %s <-> %s",
clientConn.RemoteAddr(), clientConn.LocalAddr(), dstConn.RemoteAddr())
}

// Bidirectional copy with timeout
var wg sync.WaitGroup
wg.Add(2)

// Client -> Destination
go func() {
defer wg.Done()
buf := make([]byte, 65535)
for {
clientConn.SetReadDeadline(time.Now().Add(time.Duration(t.udpTimeout) * time.Second))
n, err := clientConn.Read(buf)
if err != nil {
if t.debug && err != io.EOF {
log.Printf("[UDP] Client read error: %v", err)
}
return
}
if t.debug {
log.Printf("[UDP] Client->Dst: %d bytes", n)
}
_, err = dstConn.Write(buf[:n])
if err != nil {
if t.debug {
log.Printf("[UDP] Dst write error: %v", err)
}
return
}
}
}()

// Destination -> Client
go func() {
defer wg.Done()
buf := make([]byte, 65535)
for {
dstConn.SetReadDeadline(time.Now().Add(time.Duration(t.udpTimeout) * time.Second))
n, err := dstConn.Read(buf)
if err != nil {
if t.debug && err != io.EOF {
log.Printf("[UDP] Dst read error: %v", err)
}
return
}
if t.debug {
log.Printf("[UDP] Dst->Client: %d bytes", n)
}
_, err = clientConn.Write(buf[:n])
if err != nil {
if t.debug {
log.Printf("[UDP] Client write error: %v", err)
}
return
}
}
}()

wg.Wait()
if t.debug {
log.Printf("[UDP] Connection closed: %s", clientConn.RemoteAddr())
}
}

// listenUDPAsListener returns a net.Listener for UDP (Tailscale's way)
func listenUDPAsListener(tsServer *tsnet.Server, addr string) (net.Listener, error) {
if isTailscaleIPPortString(addr) {
// Use Listen for Tailscale addresses (not ListenPacket!)
return tsServer.Listen("udp", addr)
}
// For non-Tailscale addresses, still use standard net
return net.Listen("udp", addr)
}
115 changes: 115 additions & 0 deletions tsproxy/port_forward_udp_v3.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package tsproxy

import (
"io"
"log"
"sync"
)

// ForwardUDPV3 - simplified, always use Read/Write
func (t *TsProxy) ForwardUDPV3(bind, connect string) error {
bind = resolveTshost(t.tsServer, t.tsServer.Hostname, bind)
connect = resolveTshost(t.tsServer, t.tsServer.Hostname, connect)

ln, err := listenUDPAsListener(t.tsServer, bind)
if err != nil {
log.Printf("[UDP] Listen failed: %v", err)
return err
}
defer ln.Close()

if t.debug {
log.Printf("[UDP] Listening on %s, forwarding to %s", ln.Addr(), connect)
}

for {
conn, err := ln.Accept()
if err != nil {
log.Printf("[UDP] Accept error: %v", err)
return err
}

if t.debug {
log.Printf("[UDP] Accepted connection from %s", conn.RemoteAddr())
}

// Always use Read/Write, not ReadFrom/WriteTo
go t.handleUDPConnSimple(conn, connect)
}
}

func (t *TsProxy) handleUDPConnSimple(clientConn io.ReadWriteCloser, connect string) {
defer clientConn.Close()

dstConn, err := dialAny(t.tsServer, "udp", connect)
if err != nil {
if t.debug {
log.Printf("[UDP] Dial failed (%s): %v", connect, err)
}
return
}
defer dstConn.Close()

if t.debug {
log.Printf("[UDP] Connected: client <-> %s", connect)
}

var wg sync.WaitGroup
wg.Add(2)

// Client -> Destination
go func() {
defer wg.Done()
buf := make([]byte, 65535)
for {
n, err := clientConn.Read(buf)
if err != nil {
if t.debug && err != io.EOF {
log.Printf("[UDP] Client read error: %v", err)
}
return
}
if t.debug {
log.Printf("[UDP] Client->Dst: %d bytes", n)
}
_, err = dstConn.Write(buf[:n])
if err != nil {
if t.debug {
log.Printf("[UDP] Dst write error: %v", err)
}
return
}
}
}()

// Destination -> Client (use Write, not WriteTo!)
go func() {
defer wg.Done()
buf := make([]byte, 65535)
for {
n, err := dstConn.Read(buf)
if err != nil {
if t.debug && err != io.EOF {
log.Printf("[UDP] Dst read error: %v", err)
}
return
}
if t.debug {
log.Printf("[UDP] Dst->Client: %d bytes (using Write)", n)
}
// Use Write instead of WriteTo!
_, err = clientConn.Write(buf[:n])
if err != nil {
if t.debug {
log.Printf("[UDP] Client write error: %v", err)
}
return
}
}
}()

wg.Wait()
if t.debug {
log.Printf("[UDP] Connection closed")
}
}