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Сопоставление "входящий_порт" -> "хост:порт" для маршрутизации по портам

Обработка соединений

Файл: 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,    // из конфигурации
        Port:    d.port,       // из конфигурации
    }

    if d.config.FollowRedirect {
        // Использование назначения из метаданных исходящего соединения
        // (установлено iptables REDIRECT/TPROXY или сниффингом)
        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 (Server Name Indication) для маршрутизации на основе домена:

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: Для прозрачного UDP-проксирования на Linux серверу необходимо «подделать» адрес источника при отправке ответных пакетов. Для этого используется 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 создаёт UDP-сокет, привязанный к адресу назначения с помощью опции сокета IP_TRANSPARENT, позволяя ядру принимать пакеты, адресованные любому адресу. Ответные пакеты отправляются с этого «поддельного» адреса источника через 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(). Это передаёт читатель/записыватель напрямую и блокирует до завершения соединения:

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 блокирует до завершения исходящего обработчика

Исходный код: 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: 8080, FollowRedirect: 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 устанавливает MitmServerName и MitmAlpn11 в контексте, позволяя нижестоящим обработчикам выполнять перехват TLS.

Исходный код: proxy/dokodemo/dokodemo.go:119-123

Технический анализ для целей повторной реализации.