genet

package module
v0.0.0-...-c5cafc6 Latest Latest
Warning

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

Go to latest
Published: May 18, 2026 License: MIT Imports: 9 Imported by: 0

README

genet (g = go) - Pure Go ENet Implementation

A from-scratch, Pure Go implementation of the ENet reliable UDP networking library. No CGo, no libenet - just Go, raw UDP and a healthy dose of concurrency.

⭕ Experimental

This is not a production library (yet). It's a research project aimed at matching the ENet wire protocol so closely that a Go server can talk to a C client and vice versa. If that sounds like an odd goal, welcome aboard.

Development Approach: TDD + AI-Assisted Implementation

This project was built with a human-architected, AI-implemented workflow - not "vibe coding", but a rigorous, test-driven process.

  • I served as the architect: All design decisions - the state machine, channel model, fragmentation strategy, concurrency layout and bottleneck analysis - were mine. The LLMs did not drive the design; they executed against specifications I defined.
  • LLMs acted as the implementation layer: Models translated specifications and test expectations into Go code. They were the typist; I was the reviewer, architect and decision-maker at every step.
  • TDD drove correctness from day one: Every feature began with tests capturing the reference C implementation's behaviour. Only after tests were in place did implementation begin. The test suite grew alongside the code - over 70 scenarios covering handshakes, reliability, fragmentation, edge cases and network impairments.
  • Continuous comparison against C ENet: Protocol constants, wire format serialization, command structures and behavioural semantics were validated against the original library throughout development. The Go implementation was built to match the C behaviour byte-for-byte on the wire.

This is an experiment in rigorous AI-assisted development: not asking a model to "build me a networking library", but using it as a force-multiplier within a disciplined engineering process.

Models Used
Phase Model Role
~30-40% Gemini Early prototyping, protocol scaffolding
~50-60% DeepSeek V4 + Claude Code Full implementation, TDD cycles, refinement

The Gemini API was surprisingly expensive for this scale of work (over $60–70 in some months). DeepSeek V4 proved far more cost-effective while delivering comparable quality in a structured TDD workflow.

Future of the project

I want to make this fully compatible with ENet C. Moving forward, my focus will be on the following areas:

  • An ENet C client should be able to connect to a Go server or a Go client to an ENet server seamlessly.
  • Writing C tests and somehow interfacing them with the Go implementation.
  • Stabilizing the API for the V1 release (at least for a certain period).
  • Documenting the module flow.
  • Conducting intensive reviews of the existing AI-written code using both AI and manual methods to make it more stable (ex. security).
  • Standardizing the project contribution and review processes.
  • Identifying performance bottlenecks and writing benchmark and test sets for them.

If you are working on any of these areas, please consider opening a PR or an issue. All contributions are welcome.

Why?

ENet is a battle-tested protocol for games and real-time apps: optional reliability, channels, fragmentation, congestion control - all over plain UDP. But the reference implementation is in C and wrapping it with CGo has always been... let's say an acquired taste.

Go has a fantastic networking stack and first-class concurrency. The idea behind genet is simple: re-implement the ENet protocol in idiomatic Go, byte for byte on the wire, so you get:

  • No CGo, no cross-compilation headaches
  • Native goroutine-based networking (one Host.run() loop, tick-based service)
  • Drop-in interoperability with C ENet peers
  • The full power of Go's standard library

What's Implemented

Feature Status
3-way connection handshake Done
Session ID negotiation & validation Done
Reliable sequenced delivery (with ACKs) Done
Unreliable sequenced delivery Done
Unsequenced (unordered) delivery Done
Fragmentation & reassembly (large packets) Done
Fragment metadata validation Done
Command coalescing (multiple commands per UDP packet) Done
RTT estimation (EWMA) Done
Exponential backoff retransmission Done
Keep-alive pings Done
Peer timeout / disconnect detection Done
Throttle / congestion control Done
Bandwidth limiting Done
Multiple channels per connection (up to 255) Done
Graceful disconnect (Disconnect, DisconnectLater) Done
Immediate disconnect (DisconnectNow) Done
Peer Reset Done
Host-wide Broadcast Done
In-memory transport (for testing) Done
Network simulation transport (loss, latency, jitter) Done
Packet interception hook Done

All three delivery modes work across all channels and large payloads are automatically fragmented across MTU-sized chunks and reassembled on the other side.

What's Not (Yet) Implemented

  • IPv6 support
  • Encrypted connections (ENet's built-in ENetHost.checksum is not a real security mechanism anyway - design rich middleware system)
  • Compressed headers
  • Full bandwidth limiter (the hooks are there, actual throttling is WIP)

Installation

go get github.com/SeanTolstoyevski/genet

Zero external dependencies. The module is genet with a single sub-package genet/protocol for wire-level constants and serialization.

Quick Start

Echo Server
package main

import (
    "fmt"
    "os"

    "github.com/SeanTolstoyevski/genet"
)

func main() {
    transport, err := genet.NewUDPTransport(":9000")
    if err != nil {
        panic(err)
    }
    host := genet.NewHost(transport, 32)
    defer host.Close()

    fmt.Println("Echo server listening on :9000")

    for {
        select {
        case evt := <-host.Events:
            switch evt.Type {
            case genet.EventConnect:
                fmt.Printf("peer %d connected\n", evt.Peer.IncomingPeerID)
            case genet.EventReceive:
                fmt.Printf("echoing %d bytes to peer %d\n", len(evt.Data), evt.Peer.IncomingPeerID)
                evt.Peer.Send(evt.Data, genet.PacketFlagReliable)
            case genet.EventDisconnect:
                fmt.Printf("peer %d disconnected\n", evt.Peer.IncomingPeerID)
            }
        }
    }
}
Client
transport, _ := genet.NewUDPTransport(":0") // any port
host := genet.NewHost(transport, 1)
defer host.Close()

peer, err := host.Connect("127.0.0.1:9000", 1, 0)
if err != nil {
    panic(err)
}

for evt := range host.Events {
    if evt.Type == genet.EventConnect && evt.Peer == peer {
        break
    }
}

peer.Send([]byte("hello, genet!"), genet.PacketFlagReliable)

for evt := range host.Events {
    if evt.Type == genet.EventReceive {
        fmt.Printf("echoed back: %s\n", evt.Data)
        break
    }
}

peer.Disconnect(0)

API at a Glance

Host - the central networking entity
host := genet.NewHost(transport, maxPeers)
host.Connect("addr:port", channelCount, data)  // outbound connection
host.Broadcast(channelID, data, flags)          // send to all connected peers
host.Flush()                                    // immediately flush outgoing queue
host.ChannelLimit(limit)                        // clamp max channels per peer
host.BandwidthLimit(incoming, outgoing)          // set bandwidth caps
host.SetInterceptor(func(data []byte, addr *net.UDPAddr) bool { ... }) // drop/allow packets
host.Close()                                    // shutdown

Events arrive on host.Events (a buffered chan Event). You can also non-blocking poll with host.CheckEvents().

Peer - a connection to one remote endpoint
peer.Send(data, flags)                              // send on channel 0
peer.SendOnChannel(channelID, data, flags)           // send on a specific channel
peer.Disconnect(data)                                // graceful disconnect
peer.DisconnectLater(data)                           // disconnect after queue drains
peer.DisconnectNow(data)                             // immediate, no notification
peer.Reset()                                         // force reset
peer.Ping()                                          // manual RTT probe
peer.PingInterval(intervalMs)                        // set keep-alive interval
peer.Timeout(limit, minMs, maxMs)                    // configure timeout behaviour
peer.ThrottleConfigure(interval, accel, decel)       // adjust throttle
peer.State()                                         // current PeerState
Packet Flags
genet.PacketFlagReliable     // sequenced, acknowledged, retransmitted
genet.PacketFlagUnsequenced  // no sequencing, no retransmission - fire and forget
0                             // (no flag) - unreliable but sequenced
Events
Event When
EventConnect Peer handshake completed (client or server side)
EventReceive Data arrived on a channel (evt.ChannelID, evt.Data)
EventDisconnect Peer disconnected or timed out (evt.Data has disconnect reason)

Design

The internal architecture mirrors the C ENet structure:

┌──────────────┐     ┌──────────────────┐
│  Transport   │────▶│    Host.run()     │
│  (UDP conn)  │     │  handlePacket()   │
│              │◀────│  servicePeers()   │
└──────────────┘     └────────┬─────────┘
                              │
                     ┌────────▼─────────┐
                     │   Peer[0..N]      │
                     │  ┌─ Channel[0..N] │
                     │  │  SentQueue     │
                     │  │  IncomingQueue │
                     │  │  Fragments     │
                     │  └─────────────── │
                     │  State machine    │
                     │  RTT / Throttle   │
                     └──────────────────┘
  • Host.run() reads from the transport and ticks every 5ms (todo) to service peers (retransmissions, pings, fragment cleanup).
  • handlePacket() dispatches commands by type - handshake, data, ACK, disconnect - and validates session IDs.
  • Each Peer holds its own state machine, reliability windows, fragment reassembly maps and command queues.

Wire Compatibility

The goal is byte-level compatibility with C ENet. The handshake, command framing, sequence number arithmetic, fragmentation layout and ACK logic are all ported from the reference implementation. Session IDs increment and wrap identically. Peer IDs use the same 12-bit encoding with flag bits packed into the upper nibble.

If you find a case where a Go peer can't talk to a C peer, that's a bug - please open an issue.

Examples

The examples/ directory contains runnable programs:

  • chat/ - Multi-user chat room. Clients pick a nickname and exchange messages. Classic IRC vibes, minus the netsplits.
  • echo/ - Simple round-trip. Client sends a message, server echoes it back. The "hello world" of networking.
  • gamestate/ - Mock game server. Server ticks state updates at 20Hz (unreliable delivery), clients receive them and also send chat messages (reliable). Demonstrates mixing delivery modes.

Run them:

# Terminal 1 - chat server
go run examples/chat/server/server.go :9876

# Terminal 2, 3, ... - chat clients
go run examples/chats/client/client.go 127.0.0.1:9876

Testing

The test suite covers over 70 distinct scenarios. All tests use standard Go tooling - no external test frameworks.

Quick Commands
# All tests with race detector (recommended)
go test -race ./...

# Fuzz tests - wire format and fragment assembly (2 min per fuzz target)
go test -fuzz=<fuzz_test_name$> -fuzztime=120s .

# Fuzz only the protocol layer (faster cycle)
go test -fuzz=<fuzz_test_name$> -fuzztime=60s ./protocol/

# Benchmarks
go test -bench=. -benchmem -run=^$ .

Experimental - What That Means

  • The wire protocol is stable and tested against itself and C ENet.
  • The Go API may change. Method names, event types, constructor signatures - none of it is frozen.
  • The internal state machine is correct for the happy path but edge cases around rapid connect/disconnect cycles, zombie cleanup and resource exhaustion haven't all been explored.
  • Performance hasn't been a focus yet. There's probably a goroutine or two that could be leaner.
  • Documentation is this README plus the code. No godoc.org shrine just yet.

If you use genet in a project and it breaks, you get to keep both pieces. That said, bug reports are genuinely welcome - they make the library better.

License

MIT - do what you want, just don't sue me if your game server catches fire.

Credits

  • ENet by Lee Salzman - the original C library that made reliable UDP approachable
  • Go's net package - doing the heavy lifting so we don't have to
  • Mulberry32 PRNG - tiny, fast and good enough for sequence numbers

Documentation

Index

Constants

View Source
const (
	PeerStateDisconnected            PeerState = iota
	PeerStateConnecting                        = 1
	PeerStateAcknowledgingConnect              = 2
	PeerStateConnectionPending                 = 3
	PeerStateConnectionSucceeded               = 4
	PeerStateConnected                         = 5
	PeerStateDisconnectLater                   = 6
	PeerStateDisconnecting                     = 7
	PeerStateAcknowledgingDisconnect           = 8
	PeerStateZombie                            = 9
)

Variables

This section is empty.

Functions

func GetEventData

func GetEventData(size int) []byte

func GetPacketData

func GetPacketData() *[]byte

func PutEventData

func PutEventData(buf []byte)

func PutPacketData

func PutPacketData(buf *[]byte)

Types

type Channel

type Channel struct {
	IncomingReliableSequenceNumber   atomicUint16
	OutgoingReliableSequenceNumber   atomicUint16
	IncomingUnreliableSequenceNumber atomicUint16
	OutgoingUnreliableSequenceNumber atomicUint16
	IncomingReliableCommands         []*IncomingCommand
	IncomingUnreliableCommands       []*IncomingCommand
	// contains filtered or unexported fields
}

type Event

type Event struct {
	Type      EventType
	Peer      *Peer
	ChannelID uint8
	Data      []byte
}

func (*Event) Release

func (e *Event) Release()

Release returns the event's data buffer to the pool. Must be called by the event consumer when done with Data.

type EventType

type EventType int
const (
	EventNone       EventType = 0
	EventConnect    EventType = 1
	EventDisconnect EventType = 2
	EventReceive    EventType = 3
)

type Host

type Host struct {
	Events        chan Event
	EventsDropped atomic.Uint64

	Interceptor func(data []byte, addr *net.UDPAddr) bool

	ChannelLimitVal   uint32
	IncomingBandwidth uint32
	OutgoingBandwidth uint32
	MTU               uint32

	RandomSeed uint32
	// contains filtered or unexported fields
}

func NewHost

func NewHost(transport Transport, peerCount int) *Host

func (*Host) BandwidthLimit

func (h *Host) BandwidthLimit(incoming, outgoing uint32)

BandwidthLimit sets the bandwidth limits for the host.

func (*Host) Broadcast

func (h *Host) Broadcast(channelID uint8, data []byte, flags PacketFlag)

Broadcast sends a packet to all connected peers.

func (*Host) ChannelLimit

func (h *Host) ChannelLimit(limit uint32)

ChannelLimit sets the maximum number of channels.

func (*Host) CheckEvents

func (h *Host) CheckEvents() (Event, bool)

func (*Host) Close

func (h *Host) Close()

func (*Host) Connect

func (h *Host) Connect(address string, channelCount int, data uint32) (*Peer, error)

func (*Host) Flush

func (h *Host) Flush()

Flush immediately sends all queued outgoing commands.

func (*Host) SetInterceptor

func (h *Host) SetInterceptor(fn func(data []byte, addr *net.UDPAddr) bool)

SetInterceptor sets the packet interceptor for debugging/testing.

type IncomingCommand

type IncomingCommand struct {
	ReliableSequenceNumber   uint16
	UnreliableSequenceNumber uint16
	Command                  protocol.Command
	FragmentCount            uint32
	FragmentsRemaining       uint32
	Packet                   []byte
	Processed                bool
}

type IncomingFragmentedPacket

type IncomingFragmentedPacket struct {
	StartSequenceNumber uint16
	TotalLength         uint32
	FragmentCount       uint32
	FragmentsRemaining  uint32
	Data                []byte
	ReceivedFragments   []bool
	CreatedAt           uint32
}

type OutgoingCommand

type OutgoingCommand struct {
	Command                protocol.Command
	SentTime               uint32
	RoundTripTimeout       uint32
	ReliableSequenceNumber uint16
	SendAttempts           uint32
	ChannelID              uint8

	Payload []byte
	// contains filtered or unexported fields
}

type Packet

type Packet struct {
	Data []byte
	Addr net.Addr
	// contains filtered or unexported fields
}

func (*Packet) Release

func (p *Packet) Release()

Release returns the packet's data buffer to the pool if it came from one.

type PacketFlag

type PacketFlag uint32
const (
	PacketFlagReliable    PacketFlag = 1 << 0
	PacketFlagUnsequenced PacketFlag = 1 << 1
)

type Peer

type Peer struct {
	OutgoingQueue        []*OutgoingCommand
	SentReliableCommands []*OutgoingCommand

	OutgoingReliableSequenceNumber atomicUint16
	IncomingReliableSequenceNumber atomicUint16

	Address  *net.UDPAddr
	Host     *Host
	Channels []*Channel

	RoundTripTime                uint32
	RoundTripTimeVariance        uint32
	LowestRoundTripTime          uint32
	LastRoundTripTimeVariance    uint32
	LastRoundTripTime            uint32
	HighestRoundTripTimeVariance uint32
	LastReceiveTime              uint32
	LastSendTime                 uint32
	EarliestTimeout              uint32

	// IDs
	IncomingPeerID uint16
	OutgoingPeerID uint16 // Remote index
	ConnectID      uint32
	EventData      uint32

	// Session
	IncomingSessionID uint8
	OutgoingSessionID uint8

	// Configuration
	MTU        uint32
	WindowSize uint32

	ChannelCount               uint32
	IncomingBandwidth          uint32
	OutgoingBandwidth          uint32
	PacketThrottleInterval     uint32
	PacketThrottleAcceleration uint32
	PacketThrottleDeceleration uint32

	// Timeouts
	TimeoutLimit   uint32
	TimeoutMinimum uint32
	TimeoutMaximum uint32
	PingIntervalMs uint32

	// Unsequenced
	OutgoingUnsequencedGroup uint16
	UnsequencedWindow        [protocol.PeerUnsequencedWindowSize / 32]uint32

	// Statistics
	PacketsSent        uint32
	PacketsLost        uint32
	PacketLoss         uint32
	PacketLossVariance uint32

	// Throttle
	PacketThrottle        uint32
	PacketThrottleLimit   uint32
	PacketThrottleCounter uint32
	PacketThrottleEpoch   uint32
	// contains filtered or unexported fields
}

func NewPeer

func NewPeer(host *Host, addr *net.UDPAddr, incomingPeerID uint16) *Peer

func (*Peer) Disconnect

func (p *Peer) Disconnect(data uint32)

Disconnect sends a disconnect command to the peer.

func (*Peer) DisconnectLater

func (p *Peer) DisconnectLater(data uint32)

DisconnectLater schedules a disconnect after all queued outgoing commands are sent.

func (*Peer) DisconnectNow

func (p *Peer) DisconnectNow(data uint32)

DisconnectNow immediately disconnects the peer without notification.

func (*Peer) Ping

func (p *Peer) Ping()

Ping sends a ping command to the peer for RTT measurement.

func (*Peer) PingInterval

func (p *Peer) PingInterval(interval uint32)

PingInterval sets the ping interval for the peer.

func (*Peer) Reset

func (p *Peer) Reset()

Reset resets the peer to its initial state.

func (*Peer) Send

func (p *Peer) Send(data []byte, flags PacketFlag) error

func (*Peer) SendOnChannel

func (p *Peer) SendOnChannel(channelID uint8, data []byte, flags PacketFlag) error

SendOnChannel sends a packet on the specified channel.

func (*Peer) SetState

func (p *Peer) SetState(s PeerState)

SetState sets the peer state.

func (*Peer) State

func (p *Peer) State() PeerState

State returns the current peer state.

func (*Peer) ThrottleConfigure

func (p *Peer) ThrottleConfigure(interval, acceleration, deceleration uint32)

ThrottleConfigure sends a throttle configuration command to the peer.

func (*Peer) Timeout

func (p *Peer) Timeout(limit, min, max uint32)

Timeout sets the timeout parameters for the peer.

type PeerState

type PeerState int

type SimulatedTransport

type SimulatedTransport struct {
	PacketsDropped    int64
	PacketsSent       int64
	PacketsDuplicated int64
	PacketsDelayed    int64
	// contains filtered or unexported fields
}

SimulatedTransport is a Transport implementation that simulates network impairments: packet loss, latency, jitter, bandwidth limits, duplication, and reordering.

func NewSimulatedTransport

func NewSimulatedTransport(config SimulatedTransportConfig, bufSize int) *SimulatedTransport

NewSimulatedTransport creates a SimulatedTransport with the given config. bufSize controls the capacity of the internal packet channel.

func (*SimulatedTransport) Close

func (st *SimulatedTransport) Close() error

Close shuts down the delivery goroutine and closes channels.

func (*SimulatedTransport) Inject

func (st *SimulatedTransport) Inject(data []byte, addr net.Addr)

Inject bypasses network simulation and pushes a packet directly. Used by tests to simulate a remote peer sending data.

func (*SimulatedTransport) LocalAddr

func (st *SimulatedTransport) LocalAddr() net.Addr

LocalAddr returns a dummy address since this is an in-memory transport.

func (*SimulatedTransport) Packets

func (st *SimulatedTransport) Packets() <-chan Packet

Packets returns the channel of incoming packets (after network delay). Read by the host's run() goroutine.

func (*SimulatedTransport) Send

func (st *SimulatedTransport) Send(addr net.Addr, data []byte) error

Send applies network impairments and schedules the packet for delivery. Called by the host to send outgoing data.

func (*SimulatedTransport) SentPacketsChan

func (st *SimulatedTransport) SentPacketsChan() <-chan Packet

SentPackets returns the channel of sent packets for test observation.

func (*SimulatedTransport) SetBandwidthLimit

func (st *SimulatedTransport) SetBandwidthLimit(bytesPerSec int64)

SetBandwidthLimit dynamically adjusts bandwidth limit.

func (*SimulatedTransport) SetJitter

func (st *SimulatedTransport) SetJitter(jitter time.Duration)

SetJitter dynamically adjusts jitter.

func (*SimulatedTransport) SetLatency

func (st *SimulatedTransport) SetLatency(min, max time.Duration)

SetLatency dynamically adjusts min/max latency.

func (*SimulatedTransport) SetPacketLossRate

func (st *SimulatedTransport) SetPacketLossRate(rate float64)

SetPacketLossRate dynamically adjusts packet loss.

type SimulatedTransportConfig

type SimulatedTransportConfig struct {
	Seed           int64         // RNG seed for reproducibility (0 = random)
	PacketLossRate float64       // 0.0 – 1.0
	MinLatency     time.Duration // minimum one-way delay
	MaxLatency     time.Duration // maximum one-way delay
	Jitter         time.Duration // ± random variation added to latency
	BandwidthLimit int64         // bytes/sec, 0 = unlimited
	DuplicateRate  float64       // 0.0 – 1.0, probability of duplicating a packet
	ReorderProb    float64       // 0.0 – 1.0, probability of reordering adjacent packets
}

SimulatedTransportConfig configures network impairment parameters. Zero values mean "no impairment" for that dimension.

type Transport

type Transport interface {
	// Send sends data to the specified address.
	Send(addr net.Addr, data []byte) error

	// Close closes the transport.
	Close() error

	// Packets returns a channel for receiving packets.
	Packets() <-chan Packet

	// LocalAddr returns the local network address.
	LocalAddr() net.Addr
}

Transport defines the interface for network communication. This allows for different implementations (UDP, in-memory for testing, etc.).

type UDPTransport

type UDPTransport struct {
	// contains filtered or unexported fields
}

UDPTransport implements the Transport interface using net.UDPConn.

func NewUDPTransport

func NewUDPTransport(address string) (*UDPTransport, error)

NewUDPTransport creates a new UDPTransport listening on the specified address.

func (*UDPTransport) Close

func (t *UDPTransport) Close() error

func (*UDPTransport) LocalAddr

func (t *UDPTransport) LocalAddr() net.Addr

func (*UDPTransport) Packets

func (t *UDPTransport) Packets() <-chan Packet

func (*UDPTransport) Send

func (t *UDPTransport) Send(addr net.Addr, data []byte) error

Directories

Path Synopsis
examples
chat/client command
chat/server command
echo/client command
echo/server command

Jump to

Keyboard shortcuts

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