Skip to content

UDP-over-TCP (UoT)

Source: common/uot/router.go

Overview

UoT (UDP-over-TCP) tunnels UDP traffic through TCP connections. It intercepts connections destined for magic sentinel addresses and converts them into packet-based connections using github.com/sagernet/sing/common/uot.

Magic Addresses

Two sentinel addresses signal UoT connections:

  • uot.MagicAddress -- Current UoT protocol with request header
  • uot.LegacyMagicAddress -- Legacy UoT without request header

Router

The Router wraps an existing ConnectionRouterEx and intercepts connections by destination FQDN:

go
type Router struct {
    router adapter.ConnectionRouterEx
    logger logger.ContextLogger
}

func NewRouter(router adapter.ConnectionRouterEx, logger logger.ContextLogger) *Router {
    return &Router{router, logger}
}

Connection Handling (Ex variant)

go
func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn,
    metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
    switch metadata.Destination.Fqdn {
    case uot.MagicAddress:
        request, err := uot.ReadRequest(conn)
        if err != nil {
            N.CloseOnHandshakeFailure(conn, onClose, err)
            return
        }
        if request.IsConnect {
            r.logger.InfoContext(ctx, "inbound UoT connect connection to ", request.Destination)
        } else {
            r.logger.InfoContext(ctx, "inbound UoT connection to ", request.Destination)
        }
        metadata.Domain = metadata.Destination.Fqdn
        metadata.Destination = request.Destination
        r.router.RoutePacketConnectionEx(ctx, uot.NewConn(conn, *request), metadata, onClose)
        return

    case uot.LegacyMagicAddress:
        r.logger.InfoContext(ctx, "inbound legacy UoT connection")
        metadata.Domain = metadata.Destination.Fqdn
        metadata.Destination = M.Socksaddr{Addr: netip.IPv4Unspecified()}
        r.RoutePacketConnectionEx(ctx, uot.NewConn(conn, uot.Request{}), metadata, onClose)
        return
    }
    r.router.RouteConnectionEx(ctx, conn, metadata, onClose)
}

UoT Request Header

For the current protocol (uot.MagicAddress), a request header is read from the connection:

  • Destination: The actual UDP destination address
  • IsConnect: Boolean flag indicating connect mode vs regular mode

In connect mode, the connection behaves like a connected UDP socket to a single destination. In regular mode, each packet carries its own destination address.

Legacy Protocol

The legacy protocol (uot.LegacyMagicAddress) has no request header. The destination is set to 0.0.0.0 (IPv4 unspecified), and an empty Request{} is used.

Passthrough

Connections not matching either magic address are passed through to the underlying router unchanged:

go
r.router.RouteConnectionEx(ctx, conn, metadata, onClose)

Packet Connection Conversion

uot.NewConn(conn, request) wraps the TCP connection as a N.PacketConn. The UoT protocol frames individual UDP packets within the TCP stream, handling:

  • Packet length framing
  • Per-packet destination addressing (non-connect mode)
  • Bidirectional packet streaming

The resulting packet connection is then routed through RoutePacketConnectionEx for standard UDP processing.