goSplice

package module
v1.0.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 1, 2026 License: MIT Imports: 6 Imported by: 0

README

go-splice

Go Reference

Pure Kernel-Space TCP bridge for Go.

If you are running on Linux, this package moves bytes between two TCP sockets entirely inside the kernel using splice(2). The network payload never crosses into your Go application's memory space.

If you compile this on a Mac, Windows, or any other platform, it transparently falls back to a standard, allocation-light io.Copy. You get the exact same function signature and behavior with zero mental overhead.

n, err := goSplice.Bridge(clientConn, serverConn)

Why did we build this?

If you are building a TCP relay—like a proxy, a load balancer, a P2P sidecar, or a tunnel—your application spends almost all of its life doing one incredibly boring thing: reading bytes off one socket and writing those exact same bytes to another.

We have all written the standard way to do this:

buf := make([]byte, 32*1024)
for {
    n, err := src.Read(buf)   // kernel copies socket buffer -> your buf
    if n > 0 {
        dst.Write(buf[:n])    // kernel copies your buf -> socket buffer
    }
}

That loop is completely fine for 90% of applications. But look at what actually happens to a single chunk of data as it passes through your server:

  1. The Network Interface Card (NIC) DMAs the packet into a kernel socket receive buffer.
  2. read() copies those bytes from kernel space up into your user-space buf.
  3. Your process holds bytes it will never actually read, inspect, or modify.
  4. write() copies those exact same bytes from buf back down into a kernel socket send buffer.
  5. The NIC DMAs them out.

For a pure load balancer, steps 2 through 4 are a tollbooth. You are paying for two full memory copies and multiple context switches just to touch data you don't actually care about.

At scale, this overhead burns CPU cycles you can't get back. Even worse, in Go, it triggers massive Garbage Collection (GC) churn because you are constantly allocating and abandoning 32 KiB buffers across thousands of concurrent connections.

splice(2) removes the tollbooth. It tells the Linux kernel: "Move data from this file descriptor to that one, and don't even bother handing it to me."

No copy into your process, no copy back out, and zero GC churn. On a busy proxy, this is the difference between your throughput being choked by memory bandwidth, versus being purely bound by your network card.

The Proof is in the Benchmarks

We benchmarked this on an Intel Core Ultra 9 185H pushing data through local TCP sockets.

> go test -bench=. -benchmem
goos: linux
goarch: amd64
pkg: github.com/allenbiji/goSplice
cpu: Intel(R) Core(TM) Ultra 9 185H
BenchmarkBridgeThroughput-22    57580    19923 ns/op    1644.75 MB/s    224 B/op    10 allocs/op
PASS
ok      github.com/allenbiji/go-splice        1.369s

What these numbers actually mean for your infrastructure:

  • 1644.75 MB/s (~13.1 Gbps): You are pushing data fast enough to completely saturate a 10 Gigabit enterprise network link.
  • 224 B/op: This is the magic number. A standard io.Copy loop would allocate ~65,536 bytes per bidirectional connection. goSplice allocates just 224 bytes of structural overhead (the WaitGroup and atomic counters). The payload itself is entirely invisible to the Go heap, completely eliminating the GC churn trap.

How it works under the hood

1. The Kernel Pipe Trick

Linux has one strict rule for splice(2): you cannot move data directly from one socket to another. At least one end of a splice must be a pipe. So, a socket-to-socket move is actually two splices chained together through a kernel pipe that acts as an invisible bridge:

        splice #1                      splice #2
 socket ──────────▶ pipe (write end) ══ pipe (read end) ──────────▶ socket
 (source)          [ kernel-only buffer, never in Go RAM ]          (dest)
2. Full-Duplex Routing

TCP is bidirectional. To handle this, Bridge spins up two goroutines, each owning its own dedicated, one-way kernel pipe so the flows never collide:

                 ┌──────────────── goroutine A ────────────────┐
    clientConn ═══╡  splice ─▶ [upstream pipe] ─▶ splice        ╞═══▶ serverConn
                 └─────────────────────────────────────────────┘

                 ┌──────────────── goroutine B ────────────────┐
    clientConn ◀══╡        splice ◀─ [downstream pipe] ◀─ splice ╞═══ serverConn
                 └─────────────────────────────────────────────┘
3. Playing nice with Go's Netpoller

When you write raw system calls in Go, you risk blocking the CPU and freezing the runtime. goSplice avoids this by accessing the raw file descriptors via SyscallConn() and executing the splices in non-blocking mode (SPLICE_F_NONBLOCK).

If a socket is empty, the kernel returns EAGAIN. Instead of spinning the CPU, our code politely returns control to Go's Netpoller, which parks the goroutine to sleep until network packets actually arrive. Thousands of idle connections will cost you almost zero CPU.

Platform Support

Platform Path Mechanism
Linux Fast path splice(2) — kernel-space, zero-copy
macOS, Windows, etc. Fallback io.Copy — correct, user-space

You don't have to configure anything. This is handled entirely at compile time using Go build constraints.

Write your code on your Mac or Windows laptop, run your tests, and it will safely use io.Copy. The moment you compile that exact same code for your Linux production server, it automatically shifts into high gear and bypasses user-space.

Installation

go get github.com/allenbiji/go-splice

Note: On Linux, this package relies on golang.org/x/sys/unix.

Usage

The entire API is a single function. Just pass it two established TCP connections.

A Minimal TCP Proxy Example
package main

import (
	"log"
	"net"

	"github.com/allenbiji/goSplice"
)

func main() {
	ln, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatal(err)
	}
	defer ln.Close()
	log.Println("Proxy listening on :8080...")

	for {
		client, err := ln.Accept()
		if err != nil {
			continue
		}
		go handle(client.(*net.TCPConn)) // Assert to *net.TCPConn
	}
}

func handle(client *net.TCPConn) {
	defer client.Close()

	// You retain total control over how you dial your backends
	upstream, err := net.Dial("tcp", "127.0.0.1:9000")
	if err != nil {
		return
	}
	server := upstream.(*net.TCPConn)
	defer server.Close()

	// Hand off to the kernel
	n, err := goSplice.Bridge(client, server)
	if err != nil {
		log.Printf("bridge closed with error: %v", err)
		return
	}
	log.Printf("Session closed safely. %d bytes relayed.", n)
}

When to use it (and when you shouldn't)

Use this when:

  • You are building a pure byte relay (load balancer, VPN, forward proxy) where you don't need to look at the payload.
  • You are dealing with high throughput or thousands of concurrent connections and want to eliminate GC pauses.

Do not use this when:

  • You need to inspect, log, or manipulate the traffic. splice intentionally hides the bytes from your application.
  • You are terminating TLS inside your Go process. Splicing only works on raw plaintext bytes. If you try to splice a *tls.Conn, it will fail because the kernel cannot decrypt your user-space certificates.

Limitations to keep in mind

  • Plain TCP Only: Both ends must strictly be *net.TCPConn. You cannot pass QUIC, wrapped TLS connections, or abstract net.Conn interfaces. The system call requires a real, raw socket file descriptor.
  • Return Value Nuance: On Linux, Bridge atomically sums and returns the bytes moved in both directions. On the generic fallback (macOS/Windows), it currently only counts the Server → Client direction. If you use this return value for strict billing metrics, be aware of the cross-platform difference.
  • 32 KiB Chunking: The splice pull is capped at 32 KiB per loop to comfortably fit inside the default 64 KiB Linux pipe buffer.

Requirements

  • Go 1.17+
  • Linux: Kernel 2.6.17+ (pretty much all of them)

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Bridge

func Bridge(clientConn, serverConn *net.TCPConn) (int64, error)

Bridge splices data in both directions between two TCP connections until both directions close, then returns the total bytes relayed across both.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL