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 fails if DNS fails
}
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 帧结构的同时拆分握手数据:
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
- 通用数据包分片(
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:
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) // 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):
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 上,当传输层为不含 TLS 的原始 TCP 时,Freedom 支持 splice 零拷贝 TCP 转发:
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 处理保留逐包的目标信息,并支持带缓存的域名解析:
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 // Use cached resolution
} else {
// Resolve and cache
}
}源码: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) // Wait for response delivery
}
common.Interrupt(link.Writer)
return nil
}源码:proxy/blackhole/blackhole.go:16-43
关键行为:
- 可选地向 writer 写入响应
- 如果发送了响应,等待 1 秒以确保传递
- 中断 writer(关闭连接)
- 不 拨号任何出站连接
响应类型
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 次连接建立,采用指数退避(从 100ms 开始)。
源码:proxy/freedom/freedom.go:113
无入站:Freedom 没有实现
Inbound接口。它仅用于出站。连接空闲超时:与所有出站处理器一样,Freedom 使用
signal.CancelAfterInactivity()关闭空闲连接。分片合并模式:当
IntervalMax为 0 时,所有 TLS 分片合并为单次写入调用,而不是分别发送。这减少了系统调用,同时仍然创建多个 TLS 记录。
源码:proxy/freedom/freedom.go:520-522
Blackhole
不使用拨号器:Blackhole 完全忽略
dialer参数。它从不建立任何出站连接。中断而非关闭:处理器调用
common.Interrupt(link.Writer)而非正常关闭,这向入站处理器发出异常终止的信号。1 秒延迟:当发送了 HTTP 响应时,1 秒的等待确保响应在连接终止前到达客户端。没有这个延迟,响应可能会在内核缓冲区中丢失。
源码:proxy/blackhole/blackhole.go:38-39