Исходящие соединения Direct, Block и DNS
Эти три типа исходящих соединений выполняют фундаментальные функции маршрутизации: direct подключается к назначению без прокси, block отклоняет все соединения, а dns перехватывает DNS-трафик для внутреннего разрешения.
Исходный код: protocol/direct/outbound.go, protocol/direct/inbound.go, protocol/direct/loopback_detect.go, protocol/block/outbound.go, protocol/dns/outbound.go, protocol/dns/handle.go
Исходящее соединение Direct (Outbound)
Архитектура
type Outbound struct {
outbound.Adapter
ctx context.Context
logger logger.ContextLogger
dialer dialer.ParallelInterfaceDialer
domainStrategy C.DomainStrategy
fallbackDelay time.Duration
isEmpty bool
}Исходящее соединение direct реализует несколько интерфейсов dialer'а:
var (
_ N.ParallelDialer = (*Outbound)(nil)
_ dialer.ParallelNetworkDialer = (*Outbound)(nil)
_ dialer.DirectDialer = (*Outbound)(nil)
_ adapter.DirectRouteOutbound = (*Outbound)(nil)
)Поддержка сетей
Direct поддерживает TCP, UDP и ICMP (для ping/traceroute):
outbound.NewAdapterWithDialerOptions(C.TypeDirect, tag,
[]string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, options.DialerOptions)Ограничение Detour
Исходящее соединение direct не может использовать detour (это было бы циклическим):
if options.Detour != "" {
return nil, E.New("`detour` is not supported in direct context")
}Обнаружение пустой конфигурации (IsEmpty)
Исходящее соединение direct отслеживает, имеет ли оно нестандартную конфигурацию. Это используется маршрутизатором для оптимизации решений маршрутизации:
outbound.isEmpty = reflect.DeepEqual(options.DialerOptions, option.DialerOptions{UDPFragmentDefault: true})Установка соединения
func (h *Outbound) DialContext(ctx, network, destination) (net.Conn, error) {
ctx, metadata := adapter.ExtendContext(ctx)
metadata.Outbound = h.Tag()
metadata.Destination = destination
return h.dialer.DialContext(ctx, network, destination)
}Параллельное подключение
Исходящее соединение direct поддерживает Happy Eyeballs (параллельные попытки подключения IPv4/IPv6):
func (h *Outbound) DialParallel(ctx, network, destination, destinationAddresses) (net.Conn, error) {
return dialer.DialParallelNetwork(ctx, h.dialer, network, destination,
destinationAddresses, destinationAddresses[0].Is6(), nil, nil, nil, h.fallbackDelay)
}ICMP / Прямой маршрут
Исходящее соединение direct поддерживает ICMP-соединения для ping/traceroute через интерфейс DirectRouteOutbound:
func (h *Outbound) NewDirectRouteConnection(metadata, routeContext, timeout) (tun.DirectRouteDestination, error) {
destination, _ := ping.ConnectDestination(ctx, h.logger,
common.MustCast[*dialer.DefaultDialer](h.dialer).DialerForICMPDestination(metadata.Destination.Addr).Control,
metadata.Destination.Addr, routeContext, timeout)
return destination, nil
}Подключение с сетевой стратегией
Исходящее соединение поддерживает расширенные опции сетевой стратегии для многопутевых соединений:
func (h *Outbound) DialParallelNetwork(ctx, network, destination, destinationAddresses,
networkStrategy, networkType, fallbackNetworkType, fallbackDelay) (net.Conn, error) {
return dialer.DialParallelNetwork(ctx, h.dialer, network, destination,
destinationAddresses, destinationAddresses[0].Is6(),
networkStrategy, networkType, fallbackNetworkType, fallbackDelay)
}Входящее соединение Direct (Inbound)
Входящее соединение direct принимает сырые TCP/UDP-соединения и маршрутизирует их с опциональным переопределением назначения:
type Inbound struct {
inbound.Adapter
overrideOption int // 0=нет, 1=адрес+порт, 2=адрес, 3=порт
overrideDestination M.Socksaddr
}Опции переопределения
if options.OverrideAddress != "" && options.OverridePort != 0 {
inbound.overrideOption = 1 // Заменить и адрес, и порт
} else if options.OverrideAddress != "" {
inbound.overrideOption = 2 // Заменить только адрес
} else if options.OverridePort != 0 {
inbound.overrideOption = 3 // Заменить только порт
}Обнаружение петель (Loopback)
loopBackDetector предотвращает петли маршрутизации, отслеживая соединения:
type loopBackDetector struct {
networkManager adapter.NetworkManager
connMap map[netip.AddrPort]netip.AddrPort // TCP
packetConnMap map[uint16]uint16 // UDP (по портам)
}Он оборачивает исходящие соединения и проверяет входящие соединения по карте:
func (l *loopBackDetector) CheckConn(source, local netip.AddrPort) bool {
destination, loaded := l.connMap[source]
return loaded && destination != local
}Примечание: Обнаружение петель в настоящее время закомментировано в исходном коде, но инфраструктура сохранена.
Исходящее соединение Block (Outbound)
Простейшее исходящее соединение — оно отклоняет все соединения с EPERM:
type Outbound struct {
outbound.Adapter
logger logger.ContextLogger
}
func New(ctx, router, logger, tag, _ option.StubOptions) (adapter.Outbound, error) {
return &Outbound{
Adapter: outbound.NewAdapter(C.TypeBlock, tag, []string{N.NetworkTCP, N.NetworkUDP}, nil),
logger: logger,
}, nil
}
func (h *Outbound) DialContext(ctx, network, destination) (net.Conn, error) {
h.logger.InfoContext(ctx, "blocked connection to ", destination)
return nil, syscall.EPERM
}
func (h *Outbound) ListenPacket(ctx, destination) (net.PacketConn, error) {
h.logger.InfoContext(ctx, "blocked packet connection to ", destination)
return nil, syscall.EPERM
}Ключевые детали:
- Использует
option.StubOptions(пустую структуру), так как конфигурация не нужна - Возвращает
syscall.EPERM(не обобщённую ошибку), что может быть обнаружено вызывающими - Поддерживает как TCP, так и UDP (оба блокируются)
Исходящее соединение DNS (Outbound)
Исходящее соединение DNS перехватывает соединения, несущие DNS-трафик, и разрешает их через внутренний DNS-маршрутизатор.
Архитектура
type Outbound struct {
outbound.Adapter
router adapter.DNSRouter
logger logger.ContextLogger
}Обычный Dial не поддерживается
Исходящее соединение DNS не поддерживает обычные DialContext или ListenPacket:
func (d *Outbound) DialContext(ctx, network, destination) (net.Conn, error) {
return nil, os.ErrInvalid
}Вместо этого оно реализует NewConnectionEx и NewPacketConnectionEx для прямой обработки DNS-сообщений.
Потоковый DNS (TCP)
TCP DNS-соединения обрабатываются в цикле, читая DNS-сообщения с префиксом длины:
func (d *Outbound) NewConnectionEx(ctx, conn, metadata, onClose) {
metadata.Destination = M.Socksaddr{}
for {
conn.SetReadDeadline(time.Now().Add(C.DNSTimeout))
err := HandleStreamDNSRequest(ctx, d.router, conn, metadata)
if err != nil {
conn.Close()
return
}
}
}Формат данных потокового DNS
DNS по TCP использует 2-байтовый префикс длины:
func HandleStreamDNSRequest(ctx, router, conn, metadata) error {
// 1. Прочитать 2-байтовый префикс длины
var queryLength uint16
binary.Read(conn, binary.BigEndian, &queryLength)
// 2. Прочитать DNS-сообщение
buffer := buf.NewSize(int(queryLength))
buffer.ReadFullFrom(conn, int(queryLength))
// 3. Распаковать и маршрутизировать
var message mDNS.Msg
message.Unpack(buffer.Bytes())
// 4. Обменяться через DNS-маршрутизатор (асинхронно)
go func() {
response, _ := router.Exchange(ctx, &message, adapter.DNSQueryOptions{})
// Записать ответ с префиксом длины
binary.BigEndian.PutUint16(responseBuffer.ExtendHeader(2), uint16(len(n)))
conn.Write(responseBuffer.Bytes())
}()
}Пакетный DNS (UDP)
UDP DNS-пакеты обрабатываются конкурентно с таймаутом бездействия:
func (d *Outbound) NewPacketConnectionEx(ctx, conn, metadata, onClose) {
NewDNSPacketConnection(ctx, d.router, conn, nil, metadata)
}Обработчик пакетов:
- Читает DNS-пакеты из соединения
- Распаковывает каждый пакет как DNS-сообщение
- Обменивается через DNS-маршрутизатор в отдельной горутине
- Записывает ответ обратно с поддержкой усечения DNS
- Использует canceler с
C.DNSTimeoutдля обнаружения бездействия
go func() {
response, _ := router.Exchange(ctx, &message, adapter.DNSQueryOptions{})
responseBuffer, _ := dns.TruncateDNSMessage(&message, response, 1024)
conn.WritePacket(responseBuffer, destination)
}()Примеры конфигурации
Direct
{
"type": "direct",
"tag": "direct-out"
}Direct со стратегией домена
{
"type": "direct",
"tag": "direct-out",
"domain_strategy": "prefer_ipv4"
}Block
{
"type": "block",
"tag": "block-out"
}DNS
{
"type": "dns",
"tag": "dns-out"
}Входящее соединение Direct (Inbound) — с переопределением
{
"type": "direct",
"tag": "direct-in",
"listen": "::",
"listen_port": 5353,
"override_address": "8.8.8.8",
"override_port": 53
}