Skip to content

Freedom и Blackhole

Freedom и Blackhole — это обработчики только для исходящих соединений, находящиеся на противоположных концах спектра: Freedom подключается напрямую к назначению (как без прокси), а Blackhole молча отбрасывает весь трафик.


Freedom (прямое исходящее соединение)

Freedom — это обработчик исходящих соединений по умолчанию для прямых подключений. Он подключается к назначению напрямую, поддерживает стратегии разрешения доменных имён, фрагментацию TCP для обхода цензуры, инъекцию UDP-шума и PROXY-протокол для передачи информации о клиенте бэкендам.

Обзор

  • Направление: Только исходящий
  • Транспорт: TCP + UDP
  • Шифрование: Н/Д (прямое соединение)
  • Сценарий использования: Прямой доступ в интернет, доступ к локальной сети, финальный исходящий обработчик в цепочках маршрутизации

Поток соединения

mermaid
graph LR
    A[Решение маршрутизации] --> B[Обработчик Freedom]
    B --> C{Стратегия домена?}
    C -->|AsIs| D[Подключение как есть]
    C -->|UseIP/ForceIP| E[DNS-поиск]
    E --> D
    D --> F{Фрагментация?}
    F -->|Да| G[Записыватель фрагментов]
    F -->|Нет| H[Прямой записыватель]
    G --> I[Назначение]
    H --> I

Стратегия домена

Freedom поддерживает стратегии DNS-разрешения для исходящих соединений:

go
if h.config.DomainStrategy.HasStrategy() && dialDest.Address.Family().IsDomain() {
    ips, err := internet.LookupForIP(dialDest.Address.Domain(), strategy, outGateway)
    if err != nil && h.config.DomainStrategy.ForceIP() {
        return err  // ForceIP завершается ошибкой, если DNS недоступен
    }
    dialDest.Address = net.IPAddress(ips[dice.Roll(len(ips))])
}

Исходный код: proxy/freedom/freedom.go:114-134

Динамическая стратегия для UDP: Когда оригинальная цель — IP-адрес, а назначение было разрешено из домена, стратегия адаптируется для предпочтения того же семейства адресов:

go
if destination.Network == net.Network_UDP && origTargetAddr != nil && outGateway == nil {
    strategy = strategy.GetDynamicStrategy(origTargetAddr.Family())
}

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

Фрагментация TCP

Конфигурация Fragment разделяет сообщения TLS ClientHello на более мелкие TCP-сегменты для обхода DPI:

go
type FragmentWriter struct {
    fragment *Fragment
    writer   io.Writer
    count    uint64
}

Исходный код: proxy/freedom/freedom.go:483-487

Два режима фрагментации:

  1. Фрагментация TLS ClientHello (PacketsFrom=0, PacketsTo=1): Фрагментирует только первую TLS-запись (байт 0 должен быть 0x16 = рукопожатие). Разделяет данные рукопожатия внутри TLS-записей, сохраняя корректное TLS-форматирование:
go
if f.count != 1 || len(b) <= 5 || b[0] != 22 {
    return f.writer.Write(b)  // Не TLS или не первый пакет
}
// Разделение TLS-записи на множественные записи со случайными размерами
for from := 0; ; {
    to := from + int(crypto.RandBetween(LengthMin, LengthMax))
    // Создание нового заголовка TLS-записи для каждого фрагмента
    copy(buff[:3], b)        // Content type + version
    buff[3] = byte(l >> 8)   // Fragment length high
    buff[4] = byte(l)        // Fragment length low
    // Запись фрагмента с опциональной задержкой
}

Исходный код: proxy/freedom/freedom.go:492-545

  1. Общая фрагментация пакетов (PacketsFrom > 0): Фрагментирует пакеты с N по M на части случайного размера с опциональными задержками между ними.

Исходный код: proxy/freedom/freedom.go:547-568

Параметры фрагментации:

ПараметрОписание
PacketsFrom / PacketsToДиапазон номеров пакетов для фрагментации (0-1 для режима TLS)
LengthMin / LengthMaxДиапазон случайного размера фрагмента
IntervalMin / IntervalMaxСлучайная задержка между фрагментами (мс)
MaxSplitMin / MaxSplitMaxМаксимальное количество разделений на пакет

Инъекция UDP-шума

Freedom может инъектировать «шумовые» пакеты перед первым реальным UDP-пакетом для запутывания DPI:

go
type NoisePacketWriter struct {
    buf.Writer
    noises      []*Noise
    firstWrite  bool
    UDPOverride net.Destination
    remoteAddr  net.Address
}

Исходный код: proxy/freedom/freedom.go:423-429

Шум отправляется перед первой записью, за исключением DNS (порт 53):

go
if w.UDPOverride.Port == 53 {
    return w.Writer.WriteMultiBuffer(mb)  // Пропуск шума для DNS
}
for _, n := range w.noises {
    // Фильтрация по ApplyTo: "ipv4", "ipv6" или "ip"
    // Отправка фиксированного или случайного шумового пакета
    // Опциональная задержка между шумовыми пакетами
}

Исходный код: proxy/freedom/freedom.go:432-481

Поддержка PROXY-протокола

Freedom может добавлять заголовки PROXY-протокола (v1 или v2) при подключении к бэкендам:

go
if h.config.ProxyProtocol > 0 && h.config.ProxyProtocol <= 2 {
    version := byte(h.config.ProxyProtocol)
    srcAddr := inbound.Source.RawNetAddr()
    dstAddr := rawConn.RemoteAddr()
    header := proxyproto.HeaderProxyFromAddrs(version, srcAddr, dstAddr)
    header.WriteTo(rawConn)
}

Исходный код: proxy/freedom/freedom.go:141-150

Переопределение назначения

Freedom может переопределять адрес/порт назначения:

go
if h.config.DestinationOverride != nil {
    server := h.config.DestinationOverride.Server
    if isValidAddress(server.Address) {
        destination.Address = server.Address.AsAddress()
    }
    if server.Port != 0 {
        destination.Port = net.Port(server.Port)
    }
}

Исходный код: proxy/freedom/freedom.go:97-107

Splice (нулевое копирование)

На Linux Freedom поддерживает splice для TCP-перенаправления с нулевым копированием, когда транспорт — необработанный TCP без TLS:

go
if destination.Network == net.Network_TCP && useSplice &&
    proxy.IsRAWTransportWithoutSecurity(conn) {
    return proxy.CopyRawConnIfExist(ctx, conn, writeConn, link.Writer, timer, inTimer)
}

Исходный код: proxy/freedom/freedom.go:214-222

Флаг useSplice по умолчанию равен true и может быть управляем через переменную окружения XRAY_FREEDOM_SPLICE.

Исходный код: proxy/freedom/freedom.go:31-49

Обработка UDP-пакетов

Обработка UDP в Freedom сохраняет информацию о назначении для каждого пакета и поддерживает разрешение доменов с кешированием:

go
type PacketWriter struct {
    *internet.PacketConnWrapper
    Handler         *Handler
    UDPOverride     net.Destination
    ResolvedUDPAddr *utils.TypedSyncMap[string, net.Address]  // DNS cache
    LocalAddr       net.Address
}

Исходный код: proxy/freedom/freedom.go:340-352

Доменные имена в UDP-назначениях разрешаются и кешируются для обеспечения согласованной маршрутизации:

go
if b.UDP.Address.Family().IsDomain() {
    if ip, ok := w.ResolvedUDPAddr.Load(b.UDP.Address.Domain()); ok {
        b.UDP.Address = ip  // Использование кешированного разрешения
    } else {
        // Разрешение и кеширование
    }
}

Исходный код: proxy/freedom/freedom.go:370-401


Blackhole (пустой приёмник)

Blackhole — это минимальный исходящий обработчик, который отбрасывает весь трафик. Опционально он отправляет краткий ответ перед закрытием.

Обзор

  • Направление: Только исходящий
  • Транспорт: Н/Д
  • Сценарий использования: Блокировка назначений, блокировка рекламы, отбрасывание трафика на основе маршрутов

Обработчик

go
type Handler struct {
    response ResponseConfig
}

func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error {
    ob.Name = "blackhole"
    nBytes := h.response.WriteTo(link.Writer)
    if nBytes > 0 {
        time.Sleep(time.Second)  // Ожидание доставки ответа
    }
    common.Interrupt(link.Writer)
    return nil
}

Исходный код: proxy/blackhole/blackhole.go:16-43

Ключевое поведение:

  1. Опционально записывает ответ в записыватель
  2. Ожидает 1 секунду (если ответ был отправлен) для обеспечения доставки
  3. Прерывает записыватель (закрывает соединение)
  4. Не устанавливает никаких исходящих соединений

Типы ответов

NoneResponse (по умолчанию)

Ничего не записывает, немедленно закрывает:

go
func (*NoneResponse) WriteTo(buf.Writer) int32 { return 0 }

Исходный код: proxy/blackhole/config.go:25

HTTPResponse

Записывает HTTP-ответ 403 Forbidden:

go
const http403response = `HTTP/1.1 403 Forbidden
Connection: close
Cache-Control: max-age=3600, public
Content-Length: 0


`

func (*HTTPResponse) WriteTo(writer buf.Writer) int32 {
    b := buf.New()
    b.WriteString(http403response)
    n := b.Len()
    writer.WriteMultiBuffer(buf.MultiBuffer{b})
    return n
}

Исходный код: proxy/blackhole/config.go:8-34

Конфигурация

Тип ответа определяется полем Response в конфигурации:

go
func (c *Config) GetInternalResponse() (ResponseConfig, error) {
    if c.GetResponse() == nil {
        return new(NoneResponse), nil
    }
    config, err := c.GetResponse().GetInstance()
    return config.(ResponseConfig), nil
}

Исходный код: proxy/blackhole/config.go:37-47

Примечания по реализации

Freedom

  1. CanSpliceCopy = 1: Freedom устанавливает минимальный положительный уровень splice, поскольку он напрямую пропускает байты. В сочетании с входящим обработчиком, также поддерживающим splice, это позволяет осуществлять истинное перенаправление с нулевым копированием на Linux.

Исходный код: proxy/freedom/freedom.go:86

  1. Логика повторных попыток: Freedom повторяет установку соединения до 5 раз с экспоненциальной задержкой (начиная с 100 мс).

Исходный код: proxy/freedom/freedom.go:113

  1. Нет входящего обработчика: Freedom не реализует интерфейс Inbound. Он предназначен только для исходящих соединений.

  2. Тайм-аут простоя соединения: Как и все исходящие обработчики, Freedom использует signal.CancelAfterInactivity() для закрытия неактивных соединений.

  3. Комбинированный режим фрагментации: Когда IntervalMax равен 0, все TLS-фрагменты объединяются в один вызов записи вместо отправки по отдельности. Это уменьшает количество системных вызовов, сохраняя при этом создание множественных TLS-записей.

Исходный код: proxy/freedom/freedom.go:520-522

Blackhole

  1. Dialer не используется: Blackhole полностью игнорирует параметр dialer. Он никогда не устанавливает исходящие соединения.

  2. Прерывание, не закрытие: Обработчик вызывает common.Interrupt(link.Writer) вместо обычного закрытия, что сигнализирует аварийное завершение входящему обработчику.

  3. Задержка в 1 секунду: Когда отправляется HTTP-ответ, 1-секундное ожидание гарантирует, что ответ дойдёт до клиента до разрыва соединения. Без этого ответ может быть потерян в буфере ядра.

Исходный код: proxy/blackhole/blackhole.go:38-39

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