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