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:
- Fixed destination: Address and port specified in config
- Follow redirect: Use the original destination from iptables REDIRECT/TPROXY (obtained from the connection's outbound metadata)
- Port mapping: Map incoming port to a different destination address/port
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 --> GConfiguration
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:
| Field | Description |
|---|---|
Address | Predefined destination address |
Port | Predefined destination port |
Networks | Allowed network types (TCP, UDP, or both) |
FollowRedirect | If true, use original destination from connection metadata |
UserLevel | Policy level |
PortMap | Map of "incoming_port" -> "host:port" for per-port routing |
Connection Processing
File: proxy/dokodemo/dokodemo.go:69-192
Destination Resolution
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:
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:
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):
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():
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):
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:
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
DispatchLink
Dokodemo uses dispatcher.DispatchLink() instead of dispatcher.Dispatch(). This passes the reader/writer directly and blocks until the connection completes:
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 finishesSource: proxy/dokodemo/dokodemo.go:185-192
Usage Patterns
iptables REDIRECT (TCP)
iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 12345Config: 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)
iptables -t mangle -A PREROUTING -p udp -j TPROXY --to-port 12345 --tproxy-mark 1Config: 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
- 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
- Network validation: The
Init()function requires at least one network to be specified. An emptyNetworksslice causes an error.
Source: proxy/dokodemo/dokodemo.go:44-46
- Fallback address: If no address is configured and FollowRedirect is false, dokodemo uses the local connection address as a fallback, choosing
127.0.0.1or::1based on whether the local address contains a dot.
Source: proxy/dokodemo/dokodemo.go:79-90
Socket mark propagation: The
sockopt.Markvalue from the inbound configuration is propagated to FakeUDP sockets, ensuring proper routing table interaction on Linux.MITM support: When TLS SNI detection is active, dokodemo sets
MitmServerNameandMitmAlpn11on the context, enabling downstream handlers to perform TLS interception.
Source: proxy/dokodemo/dokodemo.go:119-123