Pure Kernel-Space bridge in Go.
Bridge copies bytes in both directions between two TCP connections until both sides close. On Linux it issues the splice(2) syscalls directly through SyscallConn(), moving payload socket-to-socket in kernel space — no per-connection buffer, no GC churn. On every other platform it falls back to io.Copy automatically.
This does not beat the standard library — it mirrors it.
io.Copybetween two*net.TCPConnalready usessplice(2)under the hood, with pooled pipes and larger per-call transfers than this package uses.go-splicereimplements the same syscalls explicitly so the mechanism is legible top to bottom. In production, plainio.Copyis usually the right call; reach for this to read and understand how the fast path actually works.
go get github.com/allenbiji/go-spliceA complete TCP proxy:
package main
import (
"log"
"net"
"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)
}Relays bytes in both directions until both directions reach EOF or error. Returns the total bytes relayed across both directions.
As each direction hits a clean EOF, Bridge half-closes the destination's write side (CloseWrite) so the peer sees an orderly end instead of a stalled, half-open connection. On a hard transport error it closes both connections so neither side waits on an OS timeout. Takes concrete *net.TCPConn values — the splice path needs the raw sockets.
On Linux, Bridge gets the raw file descriptors from each connection via SyscallConn(), creates one kernel pipe per direction with unix.Pipe, and runs the classic two-stage splice: splice(socket → pipe) then splice(pipe → socket), both with SPLICE_F_MOVE|SPLICE_F_NONBLOCK. The payload never enters user space.
The non-blocking part is what keeps it cooperative with Go's scheduler. When a socket isn't ready the kernel returns EAGAIN; the splice callback returns false, and RawConn.Read/Write park the goroutine on the netpoller until the fd is ready again — so thousands of idle connections cost almost no CPU, exactly as they would under io.Copy.
Off Linux, a //go:build !linux file provides the same Bridge signature backed by two io.Copy goroutines, so the package builds and behaves identically everywhere.
Same syscalls, two deliberate differences: the standard library requests up to 1 MiB per splice and draws pipes from a pool, while this reference implementation splices in 32 KiB chunks and allocates a fresh pipe pair per connection. Under heavy connection churn, io.Copy therefore does strictly fewer syscalls. That's the point of the comparison — it's a teaching implementation, not a faster one.
Use it (or, in production, plain io.Copy) for pure byte relays — load balancers, forward proxies, tunnels, P2P sidecars — where the payload is untouched.
Don't expect the splice path if you need to inspect, log, or modify traffic, or if you're terminating TLS. A *tls.Conn isn't a *net.TCPConn, and splice deliberately keeps the bytes out of your process.
- Linux for the
splice(2)path (usesgolang.org/x/sys/unix). Other platforms fall back toio.Copyautomatically. - Go 1.11+.
MIT
