Skip to content

Dokodemo-door

Dokodemo-door(日语"任意门")是一个仅入站的协议处理器,用于透明代理。它接受任意端口上的连接,并将其转发到配置的或动态确定的目标地址。它是 TProxy、基于重定向的透明代理和固定目标端口转发的主要机制。

概述

  • 方向:仅入站
  • 传输:TCP + UDP(可配置)
  • 加密:不适用(透明代理)
  • 认证:不适用
  • 用途:透明代理(iptables REDIRECT/TPROXY)、端口转发、DNS 拦截

架构

Dokodemo-door 没有自己的协议——它只是接收传入的连接并转发。目标地址通过以下三种机制之一确定:

  1. 固定目标:在配置中指定地址和端口
  2. 跟随重定向:使用 iptables REDIRECT/TPROXY 的原始目标地址(从连接的出站元数据中获取)
  3. 端口映射:将传入端口映射到不同的目标地址/端口
mermaid
graph TD
    A[传入连接] --> B{FollowRedirect?}
    B -->|是| C[从操作系统/元数据获取原始目标]
    B -->|否| D{配置了固定地址?}
    D -->|是| E[使用配置的地址:端口]
    D -->|否| F[使用连接本地地址]
    C --> G[分发到路由]
    E --> G
    F --> G

配置

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)
}

源码:proxy/dokodemo/dokodemo.go:33-40

关键配置字段:

字段描述
Address预定义目标地址
Port预定义目标端口
Networks允许的网络类型(TCP、UDP 或两者)
FollowRedirect如果为 true,使用连接元数据中的原始目标地址
UserLevel策略级别
PortMap"incoming_port" -> "host:port" 的映射,用于按端口路由

连接处理

文件:proxy/dokodemo/dokodemo.go:69-192

目标地址解析

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
            }
        }
    }
}

源码:proxy/dokodemo/dokodemo.go:69-128

TLS 服务器名称检测

当启用 FollowRedirect 且传入连接为 TLS 时,dokodemo 可以提取 SNI(服务器名称指示)用于基于域名的路由:

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)
    }
}

源码:proxy/dokodemo/dokodemo.go:115-124

端口映射

当配置了 PortMap 时,传入端口用于查找不同的目标地址:

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))
    }
}

源码:proxy/dokodemo/dokodemo.go:93-101

TCP 与 UDP 处理

TCP:标准的 buf.NewReader(conn)buf.NewWriter(conn)

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

源码:proxy/dokodemo/dokodemo.go:146-150

UDP 与 TProxy:对于 Linux 上的透明 UDP 代理,服务端需要在发送回复数据包时"伪造"源地址。这使用 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)
}

源码:proxy/dokodemo/dokodemo.go:160-182

FakeUDP(Linux TProxy)

文件:proxy/dokodemo/fakeudp_linux.go

在 Linux 上,FakeUDP 创建一个使用 IP_TRANSPARENT 套接字选项绑定到目标地址的 UDP 套接字,允许内核接受发往任意地址的数据包。回复数据包通过 WriteTo() 从这个"伪造的"源地址发送。

文件:proxy/dokodemo/fakeudp_other.go

在非 Linux 平台上,FakeUDP 返回错误,因为 TProxy 是 Linux 特有的功能。

PacketWriter

PacketWriter 处理将 UDP 回复数据包写回客户端,支持多个目标地址(用于来自不同服务器的 DNS 响应):

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
}

源码:proxy/dokodemo/dokodemo.go:205-210

每个唯一的目标地址获得自己的伪造 UDP 套接字。写入器按需创建新套接字:

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)
    }
}

源码:proxy/dokodemo/dokodemo.go:212-253

Dokodemo 使用 dispatcher.DispatchLink() 而非 dispatcher.Dispatch()。这直接传递 reader/writer 并阻塞直到连接完成:

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

源码:proxy/dokodemo/dokodemo.go:185-192

使用模式

iptables REDIRECT(TCP)

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

配置:FollowRedirect: true,Networks: TCP

内核将目标地址改写为 127.0.0.1:12345,但 Xray 可以通过 SO_ORIGINAL_DST 恢复原始目标地址。

iptables TPROXY(TCP + UDP)

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

配置:FollowRedirect: true,Networks: TCP+UDP,并配置适当的套接字标记。

固定目标(端口转发)

配置:Address: "10.0.0.1"Port: 8080FollowRedirect: false

监听端口上的所有传入连接都转发到 10.0.0.1:8080

实现说明

  1. CanSpliceCopy = 1:Dokodemo 设置最低的 splice 级别,因为没有协议开销——原始字节直接透传,不需要任何编码或帧封装。

源码:proxy/dokodemo/dokodemo.go:132

  1. 网络验证Init() 函数要求至少指定一个网络类型。空的 Networks 切片会导致错误。

源码:proxy/dokodemo/dokodemo.go:44-46

  1. 回退地址:如果未配置地址且 FollowRedirect 为 false,dokodemo 使用本地连接地址作为回退,根据本地地址是否包含点号选择 127.0.0.1::1

源码:proxy/dokodemo/dokodemo.go:79-90

  1. 套接字标记传播:入站配置中的 sockopt.Mark 值传播到 FakeUDP 套接字,确保在 Linux 上与路由表正确交互。

  2. MITM 支持:当 TLS SNI 检测处于活动状态时,dokodemo 在上下文中设置 MitmServerNameMitmAlpn11,使下游处理器能够执行 TLS 拦截。

源码:proxy/dokodemo/dokodemo.go:119-123

用于重新实现目的的技术分析。