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.