Freedom и Blackhole
Freedom и Blackhole — это обработчики только для исходящих соединений, находящиеся на противоположных концах спектра: Freedom подключается напрямую к назначению (как без прокси), а Blackhole молча отбрасывает весь трафик.
Freedom (прямое исходящее соединение)
Freedom — это обработчик исходящих соединений по умолчанию для прямых подключений. Он подключается к назначению напрямую, поддерживает стратегии разрешения доменных имён, фрагментацию TCP для обхода цензуры, инъекцию UDP-шума и PROXY-протокол для передачи информации о клиенте бэкендам.
Обзор
- Направление: Только исходящий
- Транспорт: TCP + UDP
- Шифрование: Н/Д (прямое соединение)
- Сценарий использования: Прямой доступ в интернет, доступ к локальной сети, финальный исходящий обработчик в цепочках маршрутизации
Поток соединения
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-разрешения для исходящих соединений:
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-адрес, а назначение было разрешено из домена, стратегия адаптируется для предпочтения того же семейства адресов:
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:
type FragmentWriter struct {
fragment *Fragment
writer io.Writer
count uint64
}Исходный код: proxy/freedom/freedom.go:483-487
Два режима фрагментации:
- Фрагментация TLS ClientHello (
PacketsFrom=0, PacketsTo=1): Фрагментирует только первую TLS-запись (байт 0 должен быть0x16= рукопожатие). Разделяет данные рукопожатия внутри TLS-записей, сохраняя корректное TLS-форматирование:
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
- Общая фрагментация пакетов (
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:
type NoisePacketWriter struct {
buf.Writer
noises []*Noise
firstWrite bool
UDPOverride net.Destination
remoteAddr net.Address
}Исходный код: proxy/freedom/freedom.go:423-429
Шум отправляется перед первой записью, за исключением DNS (порт 53):
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) при подключении к бэкендам:
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 может переопределять адрес/порт назначения:
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:
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 сохраняет информацию о назначении для каждого пакета и поддерживает разрешение доменов с кешированием:
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-назначениях разрешаются и кешируются для обеспечения согласованной маршрутизации:
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 — это минимальный исходящий обработчик, который отбрасывает весь трафик. Опционально он отправляет краткий ответ перед закрытием.
Обзор
- Направление: Только исходящий
- Транспорт: Н/Д
- Сценарий использования: Блокировка назначений, блокировка рекламы, отбрасывание трафика на основе маршрутов
Обработчик
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 секунду (если ответ был отправлен) для обеспечения доставки
- Прерывает записыватель (закрывает соединение)
- Не устанавливает никаких исходящих соединений
Типы ответов
NoneResponse (по умолчанию)
Ничего не записывает, немедленно закрывает:
func (*NoneResponse) WriteTo(buf.Writer) int32 { return 0 }Исходный код: proxy/blackhole/config.go:25
HTTPResponse
Записывает HTTP-ответ 403 Forbidden:
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 в конфигурации:
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
- CanSpliceCopy = 1: Freedom устанавливает минимальный положительный уровень splice, поскольку он напрямую пропускает байты. В сочетании с входящим обработчиком, также поддерживающим splice, это позволяет осуществлять истинное перенаправление с нулевым копированием на Linux.
Исходный код: proxy/freedom/freedom.go:86
- Логика повторных попыток: Freedom повторяет установку соединения до 5 раз с экспоненциальной задержкой (начиная с 100 мс).
Исходный код: proxy/freedom/freedom.go:113
Нет входящего обработчика: Freedom не реализует интерфейс
Inbound. Он предназначен только для исходящих соединений.Тайм-аут простоя соединения: Как и все исходящие обработчики, Freedom использует
signal.CancelAfterInactivity()для закрытия неактивных соединений.Комбинированный режим фрагментации: Когда
IntervalMaxравен 0, все TLS-фрагменты объединяются в один вызов записи вместо отправки по отдельности. Это уменьшает количество системных вызовов, сохраняя при этом создание множественных TLS-записей.
Исходный код: proxy/freedom/freedom.go:520-522
Blackhole
Dialer не используется: Blackhole полностью игнорирует параметр
dialer. Он никогда не устанавливает исходящие соединения.Прерывание, не закрытие: Обработчик вызывает
common.Interrupt(link.Writer)вместо обычного закрытия, что сигнализирует аварийное завершение входящему обработчику.Задержка в 1 секунду: Когда отправляется HTTP-ответ, 1-секундное ожидание гарантирует, что ответ дойдёт до клиента до разрыва соединения. Без этого ответ может быть потерян в буфере ядра.
Исходный код: proxy/blackhole/blackhole.go:38-39