Dokodemo-door
Dokodemo-door(日语"任意门")是一个仅入站的协议处理器,用于透明代理。它接受任意端口上的连接,并将其转发到配置的或动态确定的目标地址。它是 TProxy、基于重定向的透明代理和固定目标端口转发的主要机制。
概述
- 方向:仅入站
- 传输:TCP + UDP(可配置)
- 加密:不适用(透明代理)
- 认证:不适用
- 用途:透明代理(iptables REDIRECT/TPROXY)、端口转发、DNS 拦截
架构
Dokodemo-door 没有自己的协议——它只是接收传入的连接并转发。目标地址通过以下三种机制之一确定:
- 固定目标:在配置中指定地址和端口
- 跟随重定向:使用 iptables REDIRECT/TPROXY 的原始目标地址(从连接的出站元数据中获取)
- 端口映射:将传入端口映射到不同的目标地址/端口
graph TD
A[传入连接] --> B{FollowRedirect?}
B -->|是| C[从操作系统/元数据获取原始目标]
B -->|否| D{配置了固定地址?}
D -->|是| E[使用配置的地址:端口]
D -->|否| F[使用连接本地地址]
C --> G[分发到路由]
E --> G
F --> G配置
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
目标地址解析
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(服务器名称指示)用于基于域名的路由:
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 时,传入端口用于查找不同的目标地址:
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):
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() 实现:
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 响应):
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 套接字。写入器按需创建新套接字:
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
DispatchLink
Dokodemo 使用 dispatcher.DispatchLink() 而非 dispatcher.Dispatch()。这直接传递 reader/writer 并阻塞直到连接完成:
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)
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)
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: 8080、FollowRedirect: false
监听端口上的所有传入连接都转发到 10.0.0.1:8080。
实现说明
- CanSpliceCopy = 1:Dokodemo 设置最低的 splice 级别,因为没有协议开销——原始字节直接透传,不需要任何编码或帧封装。
源码:proxy/dokodemo/dokodemo.go:132
- 网络验证:
Init()函数要求至少指定一个网络类型。空的Networks切片会导致错误。
源码:proxy/dokodemo/dokodemo.go:44-46
- 回退地址:如果未配置地址且 FollowRedirect 为 false,dokodemo 使用本地连接地址作为回退,根据本地地址是否包含点号选择
127.0.0.1或::1。
源码:proxy/dokodemo/dokodemo.go:79-90
套接字标记传播:入站配置中的
sockopt.Mark值传播到 FakeUDP 套接字,确保在 Linux 上与路由表正确交互。MITM 支持:当 TLS SNI 检测处于活动状态时,dokodemo 在上下文中设置
MitmServerName和MitmAlpn11,使下游处理器能够执行 TLS 拦截。
源码:proxy/dokodemo/dokodemo.go:119-123