go-splice

Pure Kernel-Space TCP bridge for Go.
Bridge copies bytes in both directions between two TCP connections until both sides close. On Linux the copy runs through the Go runtime's splice(2) fast path, so payload bytes move socket-to-socket in kernel space — no per-connection buffer, no GC churn. On every other platform it falls back to the standard buffered copy automatically.
No magic here. The zero-copy path belongs to the Go standard library, not to this package. io.Copy between two *net.TCPConn already uses splice(2). Bridge is ~25 lines of glue: two io.Copy calls, a clean half-close, and byte accounting — so you don't rewrite that in every relay.
Install
go get github.com/allenbiji/go-splice
Usage
A complete TCP proxy:
package main
import (
"log"
"net"
gosplice "github.com/allenbiji/go-splice"
)
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))
}
}
func handle(client *net.TCPConn) {
defer client.Close()
up, err := net.Dial("tcp", "127.0.0.1:9000")
if err != nil {
return
}
server := up.(*net.TCPConn)
defer server.Close()
n, err := gosplice.Bridge(client, server)
if err != nil {
log.Printf("bridge closed with error: %v", err)
return
}
log.Printf("session closed, %d bytes relayed", n)
}
API
func Bridge(a, b *net.TCPConn) (int64, error)
Relays bytes in both directions between a and b until both directions reach EOF or error. Returns the total bytes relayed (a→b plus b→a) and the first non-nil error encountered.
As each direction finishes, Bridge half-closes the destination's write side (CloseWrite) so the peer sees a clean EOF instead of a reset. It returns only once both directions are done. Closing the connections themselves is left to the caller.
How it works
io.Copy(dst, src) checks whether the destination implements io.ReaderFrom (or the source implements io.WriterTo). *net.TCPConn does, and on Linux those methods take the splice(2) path: the runtime pulls a pipe from a pool, moves data socket→pipe→socket with SPLICE_F_NONBLOCK, and parks on the netpoller when a socket is empty. Bridge just runs that copy in both directions and half-closes each side when its direction ends.
You can confirm the fast path with strace -f -e trace=splice,pipe2 on any proxy built from io.Copy over TCP connections — you'll see splice() calls carrying the payload and no read/write of it into user space.
When to use
Use it for pure byte relays — load balancers, forward proxies, tunnels, P2P sidecars — where you don't touch the payload.
Don't use it (and don't expect the splice path) if you need to inspect, log, or modify the traffic, or if you're terminating TLS. A *tls.Conn isn't a *net.TCPConn, so io.Copy won't splice it.
Requirements
- Go 1.11+ for the
splice(2) fast path.
- Linux for zero-copy. Other platforms fall back to a buffered copy automatically — no build tags needed.
License
MIT