Skip to content

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

mermaid
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 --> I

Domain Strategy

Freedom supports DNS resolution strategies for outbound connections:

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))])
}

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:

go
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:

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

Source: proxy/freedom/freedom.go:483-487

Two fragmentation modes:

  1. TLS ClientHello fragmentation (PacketsFrom=0, PacketsTo=1): Only fragments the first TLS record (byte 0 must be 0x16 = handshake). Splits the handshake data within TLS records while preserving valid TLS framing:
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
}

Source: proxy/freedom/freedom.go:492-545

  1. 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:

ParameterDescription
PacketsFrom / PacketsToRange of packet numbers to fragment (0-1 for TLS mode)
LengthMin / LengthMaxRandom fragment size range
IntervalMin / IntervalMaxRandom delay between fragments (ms)
MaxSplitMin / MaxSplitMaxMaximum number of splits per packet

UDP Noise Injection

Freedom can inject "noise" packets before the first real UDP packet to confuse DPI:

go
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):

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
}

Source: proxy/freedom/freedom.go:432-481

PROXY Protocol Support

Freedom can prepend PROXY protocol headers (v1 or v2) when connecting to backends:

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)
}

Source: proxy/freedom/freedom.go:141-150

Destination Override

Freedom can override the destination address/port:

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)
    }
}

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:

go
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:

go
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:

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
    }
}

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

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
}

Source: proxy/blackhole/blackhole.go:16-43

Key behavior:

  1. Optionally write a response to the writer
  2. Sleep 1 second (if response was sent) to ensure delivery
  3. Interrupt the writer (closes the connection)
  4. Does not dial any outbound connection

Response Types

NoneResponse (Default)

Writes nothing, immediately closes:

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

Source: proxy/blackhole/config.go:25

HTTPResponse

Writes an HTTP 403 Forbidden response:

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
}

Source: proxy/blackhole/config.go:8-34

Configuration

The response type is determined by the config's Response field:

go
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

  1. 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

  1. Retry logic: Freedom retries connection establishment up to 5 times with exponential backoff (starting at 100ms).

Source: proxy/freedom/freedom.go:113

  1. No inbound: Freedom does not implement the Inbound interface. It is outbound-only.

  2. Connection idle timeout: Like all outbound handlers, Freedom uses signal.CancelAfterInactivity() to close idle connections.

  3. Fragment combined mode: When IntervalMax is 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

  1. No dialer used: Blackhole ignores the dialer parameter entirely. It never makes any outbound connection.

  2. Interrupt, not close: The handler calls common.Interrupt(link.Writer) rather than a normal close, which signals an abnormal termination to the inbound handler.

  3. 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

Technical analysis for re-implementation purposes.