Question regarding packet forwarding in wireguard-go Inbox
Technerder
technerder at gmail.com
Thu Mar 19 17:50:31 UTC 2026
Hello all,
I'm trying to setup a network with a main hub/router and numerous
nodes. The general idea is to adapt the
`http_server.go`/`http_client.go` netstack example to something akin
to a hub-spoke like model. The general network structure can be
described like so:
- 1 wireguard node acting as the central node (10.0.0.1) (code
provided below under "Hub")
- N nodes connecting to the main central node and either accessing an
HTTP server on the network or providing one to other nodes on the
network
- For the sake of this example I've just gone ahead and adapted
the previously mentioned `http_server.go`/`http_client.go` netstack
examples like so:
- 10.0.0.2 as an HTTP server (code provided below under "HTTP Server")
- 10.0.0.3 as an HTTP client (code provided below under "HTTP Client")
Hub:
```go
package main
import (
"encoding/json"
"log"
"net"
"net/netip"
"os"
"os/signal"
"syscall"
"time"
"golang.zx2c4.com/wireguard/conn
<http://golang.zx2c4.com/wireguard/conn>"
"golang.zx2c4.com/wireguard/device
<http://golang.zx2c4.com/wireguard/device>"
"golang.zx2c4.com/wireguard/ipc <http://golang.zx2c4.com/wireguard/ipc>"
"golang.zx2c4.com/wireguard/tun/netstack
<http://golang.zx2c4.com/wireguard/tun/netstack>"
"golang.zx2c4.com/wireguard/wgctrl
<http://golang.zx2c4.com/wireguard/wgctrl>"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes
<http://golang.zx2c4.com/wireguard/wgctrl/wgtypes>"
)
type Config struct {
InterfaceName string `json:"interface_name"`
ListenPort int `json:"listen_port"`
ListenAddress string `json:"listen_address"`
PublicKey string `json:"public_key"`
PrivateKey string `json:"private_key"`
Peers []struct {
Identifier string `json:"identifier"`
PublicKey string `json:"public_key"`
PresharedKey string `json:"preshared_key"`
Address string `json:"address"`
PersistentKeepAliveInterval int
`json:"persistent_keepalive_interval"`
} `json:"peers"`
}
func Load(path string) (Config, error) {
var config Config
contents, err := os.ReadFile(path)
if err != nil {
return config, err
}
return config, json.Unmarshal(contents, &config)
}
func main() {
// Parse raw config
config, err := Load("config.json")
if err != nil {
log.Printf("Error loading config: %v", err)
return
}
// Parse peers
peerDNSMappings := make(map[string]string)
peers := make([]wgtypes.PeerConfig, 0)
for _, peer := range config.Peers {
publicKey, err := wgtypes.ParseKey(peer.PublicKey)
if err != nil {
log.Printf("Error parsing public key: %v", err)
continue
}
keepAliveInterval :=
time.Duration(peer.PersistentKeepAliveInterval)
peers = append(peers, wgtypes.PeerConfig{
PublicKey: publicKey,
AllowedIPs: []net.IPNet{
{
IP: net.ParseIP(peer.Address),
Mask: net.CIDRMask(32, 32),
},
},
PersistentKeepaliveInterval: &keepAliveInterval,
})
peerDNSMappings[peer.Identifier] = peer.Address
}
// Create tunnel and device
addresses :=
[]netip.Addr{netip.MustParsePrefix(config.ListenAddress).Addr()}
tunnelDev, tnet, err := netstack.CreateNetTUN(addresses, nil,
device.DefaultMTU)
if err != nil {
log.Printf("Error creating TUN: %v", err)
return
}
logger := device.NewLogger(device.LogLevelError, "WG")
dev := device.NewDevice(tunnelDev, conn.NewDefaultBind(), logger)
// -------- Enable forwarding through custom function --------
tnet.EnableForwarding()
// -----------------------------------------------------------
if err := dev.Up(); err != nil {
log.Printf("An error occurred while setting up device:
%v\n", err)
return
}
uapi, err := ipc.UAPIOpen(config.InterfaceName)
if err != nil {
log.Printf("Error opening uapi: %v", err)
return
}
uapiListener, err := ipc.UAPIListen(config.InterfaceName, uapi)
if err != nil {
log.Printf("Failed to listen on uapi socket: %v\n", err)
return
}
go func() {
for {
con, err := uapiListener.Accept()
if err != nil {
log.Printf("Error accepting
connection: %v\n", err)
return
}
go dev.IpcHandle(con)
}
}()
client, err := wgctrl.New <https://wgctrl.New>()
if err != nil {
log.Printf("Failed to create client: %v", err)
return
}
// Setup gateway
privateKey, err := wgtypes.ParseKey(config.PrivateKey)
if err != nil {
log.Printf("Failed to parse private key: %v", err)
return
}
err = client.ConfigureDevice(config.InterfaceName, wgtypes.Config{
PrivateKey: &privateKey,
ListenPort: &config.ListenPort,
FirewallMark: nil,
ReplacePeers: false,
Peers: peers,
})
if err != nil {
log.Printf("Failed to configure device: %v\n", err)
return
}
log.Printf("Listening for connections...")
// Wait for exit signal
sc := make(chan os.Signal, 1)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
<-sc
// Cleanup
uapi.Close()
dev.Close()
}
```
HTTP Client:
```go
package main
import (
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"log"
"net/http"
"net/netip"
"time"
"golang.zx2c4.com/wireguard/conn
<http://golang.zx2c4.com/wireguard/conn>"
"golang.zx2c4.com/wireguard/device
<http://golang.zx2c4.com/wireguard/device>"
"golang.zx2c4.com/wireguard/tun/netstack
<http://golang.zx2c4.com/wireguard/tun/netstack>"
)
const (
ClientPrivateKey = "" // HTTP client private key
ServerPublicKey = "" // Hub public key
)
func DecodeBase64StringToHex(base64String string) string {
data, _ := base64.StdEncoding.DecodeString(base64String)
return hex.EncodeToString(data)
}
func main() {
tun, tnet, err :=
netstack.CreateNetTUN([]netip.Addr{netip.MustParseAddr("10.10.10.3")},
[]netip.Addr{}, 1420)
if err != nil {
log.Panic(err)
}
dev := device.NewDevice(tun, conn.NewDefaultBind(),
device.NewLogger(device.LogLevelVerbose, ""))
configString :=
fmt.Sprintf("private_key=%s\npublic_key=%s\nallowed_ip=10.10.10.0/24\nendpoint=127.0.0.1:4444\npersistent_keepalive_interval=2",
DecodeBase64StringToHex(ClientPrivateKey),
DecodeBase64StringToHex(ServerPublicKey),
)
err = dev.IpcSet(configString)
if err != nil {
log.Panic(err)
}
err = dev.Up()
if err != nil {
log.Panic(err)
}
client := http.Client{
Transport: &http.Transport{
DialContext: tnet.DialContext,
},
Timeout: 3 * time.Second,
}
for {
resp, err := client.Get("http://10.10.10.2/")
if err != nil {
log.Printf("Error: %v\n", err)
continue
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error: %v\n", err)
continue
}
log.Println(string(body))
}
}
```
HTTP Server:
```go
package main
import (
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"log"
"net"
"net/http"
"net/netip"
"golang.zx2c4.com/wireguard/conn
<http://golang.zx2c4.com/wireguard/conn>"
"golang.zx2c4.com/wireguard/device
<http://golang.zx2c4.com/wireguard/device>"
"golang.zx2c4.com/wireguard/tun/netstack
<http://golang.zx2c4.com/wireguard/tun/netstack>"
)
const (
ClientPrivateKey = "" // HTTP server private key
ServerPublicKey = "" // Hub public key
)
func DecodeBase64StringToHex(base64String string) string {
data, _ := base64.StdEncoding.DecodeString(base64String)
return hex.EncodeToString(data)
}
func main() {
tun, tnet, err :=
netstack.CreateNetTUN([]netip.Addr{netip.MustParseAddr("10.10.10.2")},
[]netip.Addr{}, 1420)
if err != nil {
log.Panic(err)
}
dev := device.NewDevice(tun, conn.NewDefaultBind(),
device.NewLogger(device.LogLevelVerbose, ""))
configString :=
fmt.Sprintf("private_key=%s\npublic_key=%s\nallowed_ip=10.10.10.0/24\nendpoint=127.0.0.1:4444\npersistent_keepalive_interval=2\n",
DecodeBase64StringToHex(ClientPrivateKey),
DecodeBase64StringToHex(ServerPublicKey),
)
err = dev.IpcSet(configString)
if err != nil {
log.Panic(err)
}
err = dev.Up()
if err != nil {
log.Panic(err)
}
listener, err := tnet.ListenTCP(&net.TCPAddr{Port: 80})
if err != nil {
log.Panicln(err)
}
http.HandleFunc("/", func(writer http.ResponseWriter, request
*http.Request) {
log.Printf("> %s - %s - %s", request.RemoteAddr,
request.URL.String(), request.UserAgent())
io.WriteString(writer, "Hello!")
})
log.Printf("Listening for connections...")
err = http.Serve(listener, nil)
if err != nil {
log.Panicln(err)
}
}
```
Now on to the problem I was facing: when I initially had this setup
both http client/server programs were able to connect to the main hub
server, but were not able to communicate with each another.
I suspected this was an issue with packet forwarding being disabled
but enabling it through the usual system route (`sysctl -w
net.ipv4.ip_forward=1`) didn't appear to enable forwarding for my
tunnel. After digging around the source of wireguard-go and seeing
`tun/netstack/tun.go` and its usage of
`gvisor.dev/gvisor/pkg/tcpip/stack
<http://gvisor.dev/gvisor/pkg/tcpip/stack>` I did some experimenting
and added some code to call `SetForwardingDefaultAndAllNICs` as shown
in the code snippets below, which appears to have resolved the
internode communication issues I was running into.
```go
func (net *Net) EnableForwarding() {
if net.hasV4 {
net.stack.SetForwardingDefaultAndAllNICs(ipv4.ProtocolNumber, true)
}
if net.hasV6 {
net.stack.SetForwardingDefaultAndAllNICs(ipv6.ProtocolNumber, true)
}
}
```
My question is about whether this was the correct approach to resolve
my internode connectivity issues, and furthermore if this solution has
any other unintended consequences I might not immediately be seeing.
I'd appreciate any and all help, thank you!
Kind Regards,
Tech
More information about the WireGuard
mailing list