brk

Go Codecov pkg.go.dev

brk

Message-level reliable UDP for Go.

brk sends byte messages over UDP without turning them into a stream. It has no external dependencies.

Comparison

Property brk raw UDP TCP QUIC
Message boundaries preserved preserved stream; the application re-frames preserved
Delivery acknowledged with retries none acknowledged acknowledged
Ordering opt-in per peer none total order per stream
Connection setup none none handshake handshake with TLS
Payload encryption none; optional HMAC integrity none none always TLS
Dependencies Go standard library Go standard library Go standard library third-party library

Install

go get github.com/donomii/brk

Quickstart

go run ./cmd/brk-demo

The command starts two retrying localhost peers on automatically selected ports and sends one message in each direction:

A received: hello from B
B received: hello from A
A stats: {Sent:2 Received:2 Acked:1 Retried:0 Duplicates:0 FailedWrites:0 Dropped:0 Expired:0 Rejected:0 AuthenticationFailures:0 InvalidPackets:0}
B stats: {Sent:2 Received:2 Acked:1 Retried:0 Duplicates:0 FailedWrites:0 Dropped:0 Expired:0 Rejected:0 AuthenticationFailures:0 InvalidPackets:0}

Demos

Run all built-in demos:

./demo.sh

Run the two-peer localhost demo:

go run ./cmd/brk-demo

Run the lossy retry demo:

go run ./cmd/brk-lossy-demo

Play the recorded lossy retry run. The receiver deliberately skips the first acknowledgement, then the sender retransmits and records one acknowledged retry.

Run the local hole-punch and keepalive demo:

./nat-demo.sh

Two-terminal chat demo:

go run ./examples/chat -ip 127.0.0.1 -port 6000 127.0.0.1 6001
go run ./examples/chat -ip 127.0.0.1 -port 6001 127.0.0.1 6000

Delivery receipts

Send accepts a validated netip.AddrPort, returns the message ID immediately, and provides one terminal result:

delivery, err := server.Send(ctx, brk.SendRequest{
	Data:   []byte("hello"),
	Target: peer,
})
if err != nil {
	panic(err)
}

result, err := delivery.Wait(ctx)
if err != nil {
	panic(err)
}
fmt.Printf("id=%s status=%s attempts=%d latency=%v error=%s\n", delivery.ID(), result.Status, result.Attempts, result.Latency, result.WriteError)

Terminal statuses are acknowledged, dropped, failed, expired, rejected, and canceled. Reason distinguishes acknowledgements, deadlines, maximum attempts, write failures, pending limits, invalid messages, and server shutdown.

The older Outgoing channel and SendMessage remain available when a receipt is not needed.

RetryConfig

Start with the defaults, then override the retry settings you want:

config := brk.DefaultRetryConfig()
config.QueueLength = 2000
config.RetryInterval = 5 * time.Second
config.AckTimeout = 2 * time.Second
config.MaxAttempts = 0
config.DuplicateTTL = 5 * time.Minute
config.MaxPending = 2000
config.BackoffMultiplier = 2
config.MaxRetryDelay = 30 * time.Second
config.JitterFraction = 0.20
config.DisableJitter = false
config.DeliveryTimeout = time.Minute
config.DisableDeliveryTimeout = false
config.AuthenticationKey = nil
config.WireVersion = brk.ProtocolV1
config.FragmentPayloadBytes = 0
config.ReassemblyTTL = 5 * time.Minute
config.OrderedDelivery = false
config.OrderingHoldTimeout = 10 * time.Second

With an authentication key, unsigned packets, legacy packets, altered packets, and packets signed with a different key are rejected. Authentication protects integrity and peer possession of the shared key; it does not encrypt payloads.

Set config.WireVersion = brk.ProtocolV2 to send the binary wire format: a 44-byte header and raw payload replace JSON and base64, leaving more room for application data under the path MTU. Receivers detect each inbound packet’s format, so mixed-version peers interoperate, and acknowledgements answer in the format of the message they acknowledge.

Stats

stats := server.Stats()
fmt.Printf("sent=%d received=%d acked=%d retried=%d duplicates=%d failed=%d dropped=%d expired=%d rejected=%d auth_failures=%d invalid=%d\n", stats.Sent, stats.Received, stats.Acked, stats.Retried, stats.Duplicates, stats.FailedWrites, stats.Dropped, stats.Expired, stats.Rejected, stats.AuthenticationFailures, stats.InvalidPackets)

Logging

Diagnostics write through the package variable Logf, which defaults to log.Printf. Point it at your own logger; the replacement must be safe for concurrent use:

brk.Logf = myLogger.Printf

STUN

STUN is optional and doesn’t run during server startup. The server method uses the live UDP socket, so the returned mapping belongs to that server:

config := brk.DefaultSTUNConfig()
address, err := server.DiscoverExternalAddress(ctx, config)
if err != nil {
	panic(err)
}
fmt.Printf("%s:%d via %s\n", address.IP, address.Port, address.Server)

STUNConfig fields:

The package-level brk.DiscoverExternalAddress(config) remains for compatibility, but it probes a temporary socket that closes before return.

Hole punching and keepalives

After peers exchange connection candidates through an out-of-band rendezvous service, each can punch the other’s candidate from its live socket:

result, err := server.PunchPeer(ctx, peer, brk.DefaultPunchConfig())
if err != nil {
	panic(err)
}
fmt.Printf("peer=%v observed=%v attempts=%d round_trip=%v\n", result.Peer, result.ObservedAddress, result.Attempts, result.RoundTrip)

PunchConfig.Attempts defaults to 8; Interval defaults to 250ms. Both must be positive.

Keep the established UDP mapping active with a caller-owned blocking loop:

err = server.KeepPeerAlive(ctx, peer, brk.DefaultKeepaliveConfig())

KeepaliveConfig.Interval defaults to 20s. Cancel ctx to stop the loop.

IPv6

Bind an IPv6 server with an explicit IPv6 literal such as ::1 or ::. LocalEndpoint, Send, SendMessageTo, live STUN, punching, and keepalives use normalized netip.AddrPort values. One server owns one address-family socket; run separate IPv4 and IPv6 servers when both families are required.

Limitations

Commands

./build.sh
./test.sh
./run.sh
./demo.sh
./nat-demo.sh
./install.sh

Release

See RELEASE_NOTES.md for v0.1.0 highlights and compatibility notes.