Freedom and Blackhole
Freedom and Blackhole are outbound-only handlers at opposite ends of the spectrum: Freedom connects directly to the destination (like no proxy), while Blackhole silently drops all traffic.
Freedom (Direct Outbound)
Freedom is the default outbound handler for direct connections. It dials the destination directly, supports domain name resolution strategies, TCP fragmentation for anti-censorship, UDP noise injection, and the PROXY protocol for forwarding client information to backends.
Overview
- Direction: Outbound only
- Transport: TCP + UDP
- Encryption: N/A (direct connection)
- Use Case: Direct internet access, local network access, final outbound in routing chains
Connection Flow
graph LR
A[Routing Decision] --> B[Freedom Handler]
B --> C{Domain Strategy?}
C -->|AsIs| D[Dial as-is]
C -->|UseIP/ForceIP| E[DNS Lookup]
E --> D
D --> F{Fragment?}
F -->|Yes| G[Fragment Writer]
F -->|No| H[Direct Writer]
G --> I[Destination]
H --> IDomain Strategy
Freedom supports DNS resolution strategies for outbound connections:
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))])
}Source: proxy/freedom/freedom.go:114-134
Dynamic strategy for UDP: When the original target is an IP and the destination was resolved from a domain, the strategy adapts to prefer the same address family:
if destination.Network == net.Network_UDP && origTargetAddr != nil && outGateway == nil {
strategy = strategy.GetDynamicStrategy(origTargetAddr.Family())
}Source: proxy/freedom/freedom.go:117-119
TCP Fragmentation
The Fragment configuration splits TLS ClientHello messages into smaller TCP segments to bypass DPI:
type FragmentWriter struct {
fragment *Fragment
writer io.Writer
count uint64
}Source: proxy/freedom/freedom.go:483-487
Two fragmentation modes:
- TLS ClientHello fragmentation (
PacketsFrom=0, PacketsTo=1): Only fragments the first TLS record (byte 0 must be0x16= handshake). Splits the handshake data within TLS records while preserving valid TLS framing:
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
}Source: proxy/freedom/freedom.go:492-545
- Generic packet fragmentation (
PacketsFrom > 0): Fragments packets N through M into random-sized chunks with optional delays between them.
Source: proxy/freedom/freedom.go:547-568
Fragment parameters:
| Parameter | Description |
|---|---|
PacketsFrom / PacketsTo | Range of packet numbers to fragment (0-1 for TLS mode) |
LengthMin / LengthMax | Random fragment size range |
IntervalMin / IntervalMax | Random delay between fragments (ms) |
MaxSplitMin / MaxSplitMax | Maximum number of splits per packet |
UDP Noise Injection
Freedom can inject "noise" packets before the first real UDP packet to confuse DPI:
type NoisePacketWriter struct {
buf.Writer
noises []*Noise
firstWrite bool
UDPOverride net.Destination
remoteAddr net.Address
}Source: proxy/freedom/freedom.go:423-429
Noise is sent before the first write, skipping DNS (port 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
}Source: proxy/freedom/freedom.go:432-481
PROXY Protocol Support
Freedom can prepend PROXY protocol headers (v1 or v2) when connecting to backends:
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)
}Source: proxy/freedom/freedom.go:141-150
Destination Override
Freedom can override the destination address/port:
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)
}
}Source: proxy/freedom/freedom.go:97-107
Splice (Zero-Copy)
On Linux, Freedom supports splice for zero-copy TCP forwarding when the transport is raw TCP without TLS:
if destination.Network == net.Network_TCP && useSplice &&
proxy.IsRAWTransportWithoutSecurity(conn) {
return proxy.CopyRawConnIfExist(ctx, conn, writeConn, link.Writer, timer, inTimer)
}Source: proxy/freedom/freedom.go:214-222
The useSplice flag defaults to true and can be controlled via the XRAY_FREEDOM_SPLICE environment variable.
Source: proxy/freedom/freedom.go:31-49
UDP Packet Handling
Freedom's UDP handling preserves per-packet destination information and supports domain resolution with caching:
type PacketWriter struct {
*internet.PacketConnWrapper
Handler *Handler
UDPOverride net.Destination
ResolvedUDPAddr *utils.TypedSyncMap[string, net.Address] // DNS cache
LocalAddr net.Address
}Source: proxy/freedom/freedom.go:340-352
Domain names in UDP destinations are resolved and cached to ensure consistent routing:
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
}
}Source: proxy/freedom/freedom.go:370-401
Blackhole (Null Sink)
Blackhole is a minimal outbound handler that drops all traffic. It optionally sends a brief response before closing.
Overview
- Direction: Outbound only
- Transport: N/A
- Use Case: Blocking destinations, ad blocking, route-based traffic dropping
Handler
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
}Source: proxy/blackhole/blackhole.go:16-43
Key behavior:
- Optionally write a response to the writer
- Sleep 1 second (if response was sent) to ensure delivery
- Interrupt the writer (closes the connection)
- Does not dial any outbound connection
Response Types
NoneResponse (Default)
Writes nothing, immediately closes:
func (*NoneResponse) WriteTo(buf.Writer) int32 { return 0 }Source: proxy/blackhole/config.go:25
HTTPResponse
Writes an HTTP 403 Forbidden response:
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
}Source: proxy/blackhole/config.go:8-34
Configuration
The response type is determined by the config's Response field:
func (c *Config) GetInternalResponse() (ResponseConfig, error) {
if c.GetResponse() == nil {
return new(NoneResponse), nil
}
config, err := c.GetResponse().GetInstance()
return config.(ResponseConfig), nil
}Source: proxy/blackhole/config.go:37-47
Implementation Notes
Freedom
- CanSpliceCopy = 1: Freedom sets the lowest positive splice level since it directly passes bytes through. When combined with an inbound that also supports splice, this enables true zero-copy forwarding on Linux.
Source: proxy/freedom/freedom.go:86
- Retry logic: Freedom retries connection establishment up to 5 times with exponential backoff (starting at 100ms).
Source: proxy/freedom/freedom.go:113
No inbound: Freedom does not implement the
Inboundinterface. It is outbound-only.Connection idle timeout: Like all outbound handlers, Freedom uses
signal.CancelAfterInactivity()to close idle connections.Fragment combined mode: When
IntervalMaxis 0, all TLS fragments are combined into a single write call instead of being sent separately. This reduces system calls while still creating multiple TLS records.
Source: proxy/freedom/freedom.go:520-522
Blackhole
No dialer used: Blackhole ignores the
dialerparameter entirely. It never makes any outbound connection.Interrupt, not close: The handler calls
common.Interrupt(link.Writer)rather than a normal close, which signals an abnormal termination to the inbound handler.1-second delay: When an HTTP response is sent, the 1-second sleep ensures the response reaches the client before the connection is terminated. Without this, the response might be lost in the kernel buffer.
Source: proxy/blackhole/blackhole.go:38-39