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