Skip to content

Direct, Block, and DNS Outbounds ​

These three outbound types serve fundamental routing functions: direct connects to the destination without a proxy, block rejects all connections, and dns intercepts DNS traffic for internal resolution.

Source: protocol/direct/outbound.go, protocol/direct/inbound.go, protocol/direct/loopback_detect.go, protocol/block/outbound.go, protocol/dns/outbound.go, protocol/dns/handle.go

Direct Outbound ​

Architecture ​

go
type Outbound struct {
    outbound.Adapter
    ctx            context.Context
    logger         logger.ContextLogger
    dialer         dialer.ParallelInterfaceDialer
    domainStrategy C.DomainStrategy
    fallbackDelay  time.Duration
    isEmpty        bool
}

The direct outbound implements multiple dialer interfaces:

go
var (
    _ N.ParallelDialer             = (*Outbound)(nil)
    _ dialer.ParallelNetworkDialer = (*Outbound)(nil)
    _ dialer.DirectDialer          = (*Outbound)(nil)
    _ adapter.DirectRouteOutbound  = (*Outbound)(nil)
)

Network Support ​

Direct supports TCP, UDP, and ICMP (for ping/traceroute):

go
outbound.NewAdapterWithDialerOptions(C.TypeDirect, tag,
    []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, options.DialerOptions)

Detour Restriction ​

Direct outbound cannot use a detour (it would be circular):

go
if options.Detour != "" {
    return nil, E.New("`detour` is not supported in direct context")
}

IsEmpty Detection ​

The direct outbound tracks whether it has non-default configuration. This is used by the router to optimize routing decisions:

go
outbound.isEmpty = reflect.DeepEqual(options.DialerOptions, option.DialerOptions{UDPFragmentDefault: true})

Connection Establishment ​

go
func (h *Outbound) DialContext(ctx, network, destination) (net.Conn, error) {
    ctx, metadata := adapter.ExtendContext(ctx)
    metadata.Outbound = h.Tag()
    metadata.Destination = destination
    return h.dialer.DialContext(ctx, network, destination)
}

Parallel Dialing ​

Direct outbound supports Happy Eyeballs (parallel IPv4/IPv6) connection attempts:

go
func (h *Outbound) DialParallel(ctx, network, destination, destinationAddresses) (net.Conn, error) {
    return dialer.DialParallelNetwork(ctx, h.dialer, network, destination,
        destinationAddresses, destinationAddresses[0].Is6(), nil, nil, nil, h.fallbackDelay)
}

ICMP / Direct Route ​

Direct outbound supports ICMP connections for ping/traceroute via the DirectRouteOutbound interface:

go
func (h *Outbound) NewDirectRouteConnection(metadata, routeContext, timeout) (tun.DirectRouteDestination, error) {
    destination, _ := ping.ConnectDestination(ctx, h.logger,
        common.MustCast[*dialer.DefaultDialer](h.dialer).DialerForICMPDestination(metadata.Destination.Addr).Control,
        metadata.Destination.Addr, routeContext, timeout)
    return destination, nil
}

Network Strategy Dialing ​

The outbound supports advanced network strategy options for multi-path connections:

go
func (h *Outbound) DialParallelNetwork(ctx, network, destination, destinationAddresses,
    networkStrategy, networkType, fallbackNetworkType, fallbackDelay) (net.Conn, error) {
    return dialer.DialParallelNetwork(ctx, h.dialer, network, destination,
        destinationAddresses, destinationAddresses[0].Is6(),
        networkStrategy, networkType, fallbackNetworkType, fallbackDelay)
}

Direct Inbound ​

The direct inbound accepts raw TCP/UDP connections and routes them with an optional destination override:

go
type Inbound struct {
    inbound.Adapter
    overrideOption      int    // 0=none, 1=address+port, 2=address, 3=port
    overrideDestination M.Socksaddr
}

Override Options ​

go
if options.OverrideAddress != "" && options.OverridePort != 0 {
    inbound.overrideOption = 1  // Replace both address and port
} else if options.OverrideAddress != "" {
    inbound.overrideOption = 2  // Replace address only
} else if options.OverridePort != 0 {
    inbound.overrideOption = 3  // Replace port only
}

Loopback Detection ​

The loopBackDetector prevents routing loops by tracking connections:

go
type loopBackDetector struct {
    networkManager   adapter.NetworkManager
    connMap          map[netip.AddrPort]netip.AddrPort    // TCP
    packetConnMap    map[uint16]uint16                     // UDP (port-based)
}

It wraps outgoing connections and checks incoming connections against the map:

go
func (l *loopBackDetector) CheckConn(source, local netip.AddrPort) bool {
    destination, loaded := l.connMap[source]
    return loaded && destination != local
}

Note: Loopback detection is currently commented out in the source code but the infrastructure remains.

Block Outbound ​

The simplest outbound -- it rejects all connections with EPERM:

go
type Outbound struct {
    outbound.Adapter
    logger logger.ContextLogger
}

func New(ctx, router, logger, tag, _ option.StubOptions) (adapter.Outbound, error) {
    return &Outbound{
        Adapter: outbound.NewAdapter(C.TypeBlock, tag, []string{N.NetworkTCP, N.NetworkUDP}, nil),
        logger:  logger,
    }, nil
}

func (h *Outbound) DialContext(ctx, network, destination) (net.Conn, error) {
    h.logger.InfoContext(ctx, "blocked connection to ", destination)
    return nil, syscall.EPERM
}

func (h *Outbound) ListenPacket(ctx, destination) (net.PacketConn, error) {
    h.logger.InfoContext(ctx, "blocked packet connection to ", destination)
    return nil, syscall.EPERM
}

Key details:

  • Uses option.StubOptions (empty struct) since no configuration is needed
  • Returns syscall.EPERM (not a generic error), which can be detected by callers
  • Supports both TCP and UDP (both are blocked)

DNS Outbound ​

The DNS outbound intercepts connections that carry DNS traffic and resolves them using the internal DNS router.

Architecture ​

go
type Outbound struct {
    outbound.Adapter
    router adapter.DNSRouter
    logger logger.ContextLogger
}

Regular Dial is Unsupported ​

The DNS outbound does not support regular DialContext or ListenPacket:

go
func (d *Outbound) DialContext(ctx, network, destination) (net.Conn, error) {
    return nil, os.ErrInvalid
}

Instead, it implements NewConnectionEx and NewPacketConnectionEx to process DNS messages directly.

Stream DNS (TCP) ​

TCP DNS connections are processed in a loop, reading length-prefixed DNS messages:

go
func (d *Outbound) NewConnectionEx(ctx, conn, metadata, onClose) {
    metadata.Destination = M.Socksaddr{}
    for {
        conn.SetReadDeadline(time.Now().Add(C.DNSTimeout))
        err := HandleStreamDNSRequest(ctx, d.router, conn, metadata)
        if err != nil {
            conn.Close()
            return
        }
    }
}

Stream DNS Wire Format ​

DNS over TCP uses a 2-byte length prefix:

go
func HandleStreamDNSRequest(ctx, router, conn, metadata) error {
    // 1. Read 2-byte length prefix
    var queryLength uint16
    binary.Read(conn, binary.BigEndian, &queryLength)

    // 2. Read the DNS message
    buffer := buf.NewSize(int(queryLength))
    buffer.ReadFullFrom(conn, int(queryLength))

    // 3. Unpack and route
    var message mDNS.Msg
    message.Unpack(buffer.Bytes())

    // 4. Exchange via DNS router (async)
    go func() {
        response, _ := router.Exchange(ctx, &message, adapter.DNSQueryOptions{})
        // Write length-prefixed response
        binary.BigEndian.PutUint16(responseBuffer.ExtendHeader(2), uint16(len(n)))
        conn.Write(responseBuffer.Bytes())
    }()
}

Packet DNS (UDP) ​

UDP DNS packets are processed concurrently with an idle timeout:

go
func (d *Outbound) NewPacketConnectionEx(ctx, conn, metadata, onClose) {
    NewDNSPacketConnection(ctx, d.router, conn, nil, metadata)
}

The packet handler:

  1. Reads DNS packets from the connection
  2. Unpacks each packet as a DNS message
  3. Exchanges via the DNS router in a goroutine
  4. Writes the response back with DNS truncation support
  5. Uses a canceler with C.DNSTimeout for idle detection
go
go func() {
    response, _ := router.Exchange(ctx, &message, adapter.DNSQueryOptions{})
    responseBuffer, _ := dns.TruncateDNSMessage(&message, response, 1024)
    conn.WritePacket(responseBuffer, destination)
}()

Configuration Examples ​

Direct ​

json
{
  "type": "direct",
  "tag": "direct-out"
}

Direct with Domain Strategy ​

json
{
  "type": "direct",
  "tag": "direct-out",
  "domain_strategy": "prefer_ipv4"
}

Block ​

json
{
  "type": "block",
  "tag": "block-out"
}

DNS ​

json
{
  "type": "dns",
  "tag": "dns-out"
}

Direct Inbound (with override) ​

json
{
  "type": "direct",
  "tag": "direct-in",
  "listen": "::",
  "listen_port": 5353,
  "override_address": "8.8.8.8",
  "override_port": 53
}