Seamless UDP hole punching library using WireGuard for secure NAT traversal
  • Go 99.7%
  • Makefile 0.3%
Find a file
2026-05-06 22:46:09 +02:00
.github/workflows Move make receipts to mise 2026-05-06 19:33:50 +02:00
cmd Bump linter to 2.9.0 to match Go version compatibility 2026-05-06 19:33:50 +02:00
pkg Migrate linter to v2 2026-05-06 19:33:50 +02:00
.gitignore Hello World! 2025-04-07 00:38:49 +03:00
.golangci.yml Migrate linter config from v1 to v2 2026-05-06 19:33:50 +02:00
go.mod Bump go.mod 2026-05-06 19:33:50 +02:00
go.sum Bump all dependencies 2026-05-03 20:47:44 +02:00
LICENSE chore: add LICENSE 2025-04-24 18:19:05 +02:00
Makefile Move make receipts to mise 2026-05-06 19:33:50 +02:00
mise.toml Update dependency golangci-lint to v2.11.4 2026-05-06 22:46:09 +02:00
README.md Start using mise for CI/CD 2026-05-06 19:08:08 +02:00
renovate.json Adjust renovate 2026-05-06 19:35:58 +02:00

Alt text

About

wg-punch is a library for seamless NAT hole punching via UDP and WireGuard. It operates with a userspace TCP/IP stack, facilitating peer-to-peer communication by punching through NATs and firewalls over UDP, while WireGuard establishes L3 encrypted tunnels and overlay networks for private, secure connections.

Extension

This library is designed to be customizable and extensible. It supports switching VPN tunnel implementations, allowing you to easily swap the current tunnel for others. The original implementation uses WireGuard in userspace, but you can switch to alternative algorithms, such as the WireGuard kernel implementation (wg-punch-kernel), OpenVPN, IPSec, or any other tunneling protocol by extending the tunnel interface in pkg/tunnel/tunnel.go.

todo(): move peer-hub interface definition to this library aswell.

Additionally, the library supports customizable synchronization by implementing the Rendezvous client interface from peer-hub and integrating your own backend.

Sample usage

package main

const (
	TunnelHandshakeTimeout = 30 * time.Second
	RendezvousServer       = "http://rendezvous.yago.ninja:7777"
	
	LocalPeerID  = "o1"
	RemotePeerID = "o2"
	
	WGLocalListenPort    = 51821
	WGLocalIfaceName     = "wg1"
	WGLocalIfaceAddr     = "10.1.1.1"
	WGLocalIfaceAddrCIDR = "10.1.1.1/32" 
	
	RemotePeerIP = "10.1.1.2"
)

func main() {
	// ... 

	puncherOptions := []puncher.Option{
		puncher.WithPuncherInterval(300 * time.Millisecond),
		puncher.WithSTUNServers(stunServers),
		puncher.WithLogger(logger),
	}
	// Create a puncher with the STUN servers
	p := puncher.NewPuncher(puncherOptions...)

	connectorOptions := []connect.Option{
		connect.WithRendezServer(RendezvousServer),
		connect.WithWaitInterval(1 * time.Second),
		connect.WithLogger(logger),
	}
	// Create a connector with the puncher
	conn := connect.NewConnector(LocalPeerID, p, connectorOptions...)

	ctxHandshake, cancel := context.WithTimeout(context.Background(), TunnelHandshakeTimeout)
	defer cancel()

	tunnelCfg := &tunnel.Config{
		PrivKey:           WGLocalPrivKey,
		Iface:             WGLocalIfaceName,
		IfaceIPv4CIDR:     WGLocalIfaceAddrCIDR,
		ListenPort:        WGLocalListenPort,
		ReplacePeer:       true,
		CreateIface:       true,
		KeepAliveInterval: WGKeepAliveInterval,
	}

	// Initialize WireGuard interface using WireGuard
	tunnel, err := wguserspace.New(tunnelCfg, logger)
	if err != nil {
		logger.Error(err, "failed to create tunnel", "localPeer", LocalPeerID)
		return
	}

	// Connect to peer using a shared peer ID (both sides use same ID)
	netConn, err := conn.Connect(ctxHandshake, tunnel, []string{WGLocalIfaceAddrCIDR}, RemotePeerID)
	if err != nil {
		logger.Error(err, "failed to connect to peer", "localPeer", LocalPeerID, "remotePeerID", RemotePeerID)
		return
	}

	// todo(): think about where to put the cancel of the tunnel itself
	defer tunnel.Stop(context.Background())
	defer netConn.Close()

	logger.Infof("Tunnel has been stablished! Press Ctrl+C to exit.")

	// ...
	// Start TCP server 
	tcpServer, err := common.NewTCPServer(WGLocalIfaceAddr, TCPServerPort, logger)
	if err != nil {
		logger.Error(err, "failed to create TCP server", "address", WGLocalIfaceAddr)
		return
	}
}

Quickstart

Install development tools:

$ mise install

Run local checks:

$ mise run all

Start peer-hub server:

$ 

Start peer A node:

$ go run cmd/peerA/peer-a.go 

Start peer B node:

$ go run cmd/peerB/peer-b.go 

Detecting NAT type

$ sudo apt install stun-client
$ stun stun.l.google.com:19302
STUN client version 0.97
Primary: Independent Mapping, Independent Filter, preserves ports, will hairpin
Return value is 0x000003