Skip to content

Dokodemo-door

Dokodemo-door ("anywhere door" in Japanese) is an inbound-only protocol handler for transparent proxying. It accepts connections on any port and forwards them to a configured or dynamically determined destination. It is the primary mechanism for TProxy, redirect-based transparent proxying, and fixed-destination port forwarding.

Overview

  • Direction: Inbound only
  • Transport: TCP + UDP (configurable)
  • Encryption: N/A (transparent)
  • Authentication: N/A
  • Use Cases: Transparent proxy (iptables REDIRECT/TPROXY), port forwarding, DNS interception

Architecture

Dokodemo-door has no protocol of its own -- it simply takes incoming connections and forwards them. The destination is determined by one of three mechanisms:

  1. Fixed destination: Address and port specified in config
  2. Follow redirect: Use the original destination from iptables REDIRECT/TPROXY (obtained from the connection's outbound metadata)
  3. Port mapping: Map incoming port to a different destination address/port
mermaid
graph TD
    A[Incoming Connection] --> B{FollowRedirect?}
    B -->|Yes| C[Get original dest from OS/metadata]
    B -->|No| D{Fixed address configured?}
    D -->|Yes| E[Use config address:port]
    D -->|No| F[Use connection local address]
    C --> G[Dispatch to routing]
    E --> G
    F --> G

Configuration

go
type DokodemoDoor struct {
    policyManager policy.Manager
    config        *Config
    address       net.Address   // Fixed destination address
    port          net.Port      // Fixed destination port
    portMap       map[string]string  // Port remapping
    sockopt       *session.Sockopt   // Socket options (for mark)
}

Source: proxy/dokodemo/dokodemo.go:33-40

Key config fields:

FieldDescription
AddressPredefined destination address
PortPredefined destination port
NetworksAllowed network types (TCP, UDP, or both)
FollowRedirectIf true, use original destination from connection metadata
UserLevelPolicy level
PortMapMap of "incoming_port" -> "host:port" for per-port routing

Connection Processing

File: proxy/dokodemo/dokodemo.go:69-192

Destination Resolution

go
func (d *DokodemoDoor) Process(ctx context.Context, network net.Network,
    conn stat.Connection, dispatcher routing.Dispatcher) error {

    dest := net.Destination{
        Network: network,
        Address: d.address,    // from config
        Port:    d.port,       // from config
    }

    if d.config.FollowRedirect {
        // Use the destination from outbound metadata
        // (set by iptables REDIRECT/TPROXY or sniffing)
        outbounds := session.OutboundsFromContext(ctx)
        if len(outbounds) > 0 {
            ob := outbounds[len(outbounds)-1]
            if ob.Target.IsValid() {
                dest = ob.Target
            }
        }
    }
}

Source: proxy/dokodemo/dokodemo.go:69-128

TLS Server Name Detection

When FollowRedirect is enabled and the incoming connection is TLS, dokodemo can extract the SNI (Server Name Indication) for domain-based routing:

go
if tlsConn, ok := iConn.(tls.Interface); ok && !destinationOverridden {
    if serverName := tlsConn.HandshakeContextServerName(ctx); serverName != "" {
        dest.Address = net.DomainAddress(serverName)
        destinationOverridden = true
        ctx = session.ContextWithMitmServerName(ctx, serverName)
    }
}

Source: proxy/dokodemo/dokodemo.go:115-124

Port Mapping

When a PortMap is configured, the incoming port is used to look up a different destination:

go
if d.portMap != nil && d.portMap[port] != "" {
    h, p, _ := net.SplitHostPort(d.portMap[port])
    if len(h) > 0 {
        dest.Address = net.ParseAddress(h)
    }
    if len(p) > 0 {
        dest.Port = net.Port(strconv.Atoi(p))
    }
}

Source: proxy/dokodemo/dokodemo.go:93-101

TCP vs UDP Handling

TCP: Standard buf.NewReader(conn) and buf.NewWriter(conn):

go
if dest.Network == net.Network_TCP {
    reader = buf.NewReader(conn)
} else {
    reader = buf.NewPacketReader(conn)
}

Source: proxy/dokodemo/dokodemo.go:146-150

UDP with TProxy: For transparent UDP proxying on Linux, the server needs to "fake" the source address when sending reply packets. This uses FakeUDP():

go
if destinationOverridden {
    back := conn.RemoteAddr().(*net.UDPAddr)
    addr := &net.UDPAddr{
        IP:   dest.Address.IP(),
        Port: int(dest.Port),
    }
    pConn, err := FakeUDP(addr, mark)
    writer = NewPacketWriter(pConn, &dest, mark, back)
}

Source: proxy/dokodemo/dokodemo.go:160-182

FakeUDP (Linux TProxy)

File: proxy/dokodemo/fakeudp_linux.go

On Linux, FakeUDP creates a UDP socket bound to the destination address using IP_TRANSPARENT socket option, allowing the kernel to accept packets destined for any address. Reply packets are sent from this "faked" source address using WriteTo().

File: proxy/dokodemo/fakeudp_other.go

On non-Linux platforms, FakeUDP returns an error since TProxy is Linux-specific.

PacketWriter

The PacketWriter handles writing UDP reply packets back to the client, supporting multiple destination addresses (for DNS responses from different servers):

go
type PacketWriter struct {
    conn  net.PacketConn
    conns map[net.Destination]net.PacketConn  // cached per-dest fake sockets
    mark  int                                   // SO_MARK value
    back  *net.UDPAddr                          // client's address
}

Source: proxy/dokodemo/dokodemo.go:205-210

Each unique destination gets its own fake UDP socket. The writer creates new sockets on demand:

go
func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
    if b.UDP != nil && b.UDP.Address.Family().IsIP() {
        conn := w.conns[*b.UDP]
        if conn == nil {
            conn, _ = FakeUDP(&net.UDPAddr{IP: b.UDP.Address.IP(), Port: int(b.UDP.Port)}, w.mark)
            w.conns[*b.UDP] = conn
        }
        conn.WriteTo(b.Bytes(), w.back)
    }
}

Source: proxy/dokodemo/dokodemo.go:212-253

Dokodemo uses dispatcher.DispatchLink() instead of dispatcher.Dispatch(). This passes the reader/writer directly and blocks until the connection completes:

go
if err := dispatcher.DispatchLink(ctx, dest, &transport.Link{
    Reader: reader,
    Writer: writer,
}); err != nil {
    return errors.New("failed to dispatch request").Base(err)
}
return nil  // DispatchLink blocks until outbound finishes

Source: proxy/dokodemo/dokodemo.go:185-192

Usage Patterns

iptables REDIRECT (TCP)

bash
iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 12345

Config: FollowRedirect: true, Networks: TCP

The kernel rewrites the destination to 127.0.0.1:12345, but Xray can recover the original destination from SO_ORIGINAL_DST.

iptables TPROXY (TCP + UDP)

bash
iptables -t mangle -A PREROUTING -p udp -j TPROXY --to-port 12345 --tproxy-mark 1

Config: FollowRedirect: true, Networks: TCP+UDP, with appropriate socket marks.

Fixed Destination (Port Forwarding)

Config: Address: "10.0.0.1", Port: 8080, FollowRedirect: false

All incoming connections on the listening port are forwarded to 10.0.0.1:8080.

Implementation Notes

  1. CanSpliceCopy = 1: Dokodemo sets the lowest splice level since there is no protocol overhead -- raw bytes pass through without any encoding or framing.

Source: proxy/dokodemo/dokodemo.go:132

  1. Network validation: The Init() function requires at least one network to be specified. An empty Networks slice causes an error.

Source: proxy/dokodemo/dokodemo.go:44-46

  1. Fallback address: If no address is configured and FollowRedirect is false, dokodemo uses the local connection address as a fallback, choosing 127.0.0.1 or ::1 based on whether the local address contains a dot.

Source: proxy/dokodemo/dokodemo.go:79-90

  1. Socket mark propagation: The sockopt.Mark value from the inbound configuration is propagated to FakeUDP sockets, ensuring proper routing table interaction on Linux.

  2. MITM support: When TLS SNI detection is active, dokodemo sets MitmServerName and MitmAlpn11 on the context, enabling downstream handlers to perform TLS interception.

Source: proxy/dokodemo/dokodemo.go:119-123

Technical analysis for re-implementation purposes.