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 fails if DNS fails
    }
    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 帧结构的同时拆分握手数据:
go
if f.count != 1 || len(b) <= 5 || b[0] != 22 {
    return f.writer.Write(b)  // Not TLS or not first packet
}
// Split TLS record into multiple records with random sizes
for from := 0; ; {
    to := from + int(crypto.RandBetween(LengthMin, LengthMax))
    // Create new TLS record header for each fragment
    copy(buff[:3], b)        // Content type + version
    buff[3] = byte(l >> 8)   // Fragment length high
    buff[4] = byte(l)        // Fragment length low
    // Write fragment with optional delay
}

源码:proxy/freedom/freedom.go:492-545

  1. 通用数据包分片PacketsFrom > 0):将第 N 到第 M 个数据包分片为随机大小的块,可在块之间添加可选延迟。

源码:proxy/freedom/freedom.go:547-568

分片参数

参数描述
PacketsFrom / PacketsTo要分片的数据包编号范围(TLS 模式为 0-1)
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)  // Skip noise for DNS
}
for _, n := range w.noises {
    // Filter by ApplyTo: "ipv4", "ipv6", or "ip"
    // Send fixed or random noise packet
    // Optional delay between noise packets
}

源码: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 上,当传输层为不含 TLS 的原始 TCP 时,Freedom 支持 splice 零拷贝 TCP 转发:

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 数据包处理

Freedom 的 UDP 处理保留逐包的目标信息,并支持带缓存的域名解析:

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  // Use cached resolution
    } else {
        // Resolve and cache
    }
}

源码: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)  // Wait for response delivery
    }
    common.Interrupt(link.Writer)
    return nil
}

源码:proxy/blackhole/blackhole.go:16-43

关键行为:

  1. 可选地向 writer 写入响应
  2. 如果发送了响应,等待 1 秒以确保传递
  3. 中断 writer(关闭连接)
  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 次连接建立,采用指数退避(从 100ms 开始)。

源码: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. 不使用拨号器:Blackhole 完全忽略 dialer 参数。它从不建立任何出站连接。

  2. 中断而非关闭:处理器调用 common.Interrupt(link.Writer) 而非正常关闭,这向入站处理器发出异常终止的信号。

  3. 1 秒延迟:当发送了 HTTP 响应时,1 秒的等待确保响应在连接终止前到达客户端。没有这个延迟,响应可能会在内核缓冲区中丢失。

源码:proxy/blackhole/blackhole.go:38-39

用于重新实现目的的技术分析。