netbirdio/netbird · error
failed to close wireguard interface %s: %w
Error message
failed to close wireguard interface %s: %w
What it means
Emitted from WGIface.Close() when tun.Close() fails during teardown. Close() first frees the WireGuard proxy, then deliberately releases w.mu before closing (wireguard-go's device.Close waits for goroutines that re-enter WGIface methods), and appends the close error to a multierror that also carries later teardown failures. It means the TUN/userspace device could not be closed cleanly, typically an already-invalidated fd or a double close.
Source
Thrown at client/iface/iface.go:230
func (w *WGIface) Close() error {
w.mu.Lock()
var result *multierror.Error
if err := w.wgProxyFactory.Free(); err != nil {
result = multierror.Append(result, fmt.Errorf("failed to free WireGuard proxy: %w", err))
}
// Release w.mu before calling w.tun.Close(): the underlying
// wireguard-go device.Close() waits for its send/receive goroutines
// to drain. Some of those goroutines re-enter WGIface methods that
// take w.mu (e.g. the packet filter DNS hook calls GetDevice()), so
// holding the mutex here would deadlock the shutdown path.
tun := w.tun
w.mu.Unlock()
if err := tun.Close(); err != nil {
result = multierror.Append(result, fmt.Errorf("failed to close wireguard interface %s: %w", w.Name(), err))
}
if nbnetstack.IsEnabled() {
return errors.FormatErrorOrNil(result)
}
if err := w.waitUntilRemoved(); err != nil {
log.Warnf("failed to remove WireGuard interface %s: %v", w.Name(), err)
if err := w.Destroy(); err != nil {
result = multierror.Append(result, fmt.Errorf("failed to remove WireGuard interface %s: %w", w.Name(), err))
return errors.FormatErrorOrNil(result)
}
log.Infof("interface %s successfully removed", w.Name())
}
return errors.FormatErrorOrNil(result)
}
View on GitHub (pinned to 93e97f4bf1)
Solutions
- Ensure Close runs exactly once per interface lifecycle (the engine already sequences it)
- Run `netbird down` before stopping, rebuilding, or restarting the daemon
- Read the preceding log lines: the underlying Close error and wireguard-go device errors appear just before
- If it recurs, verify no goroutine still writes to the TUN after Close begins
Example fix
// before
w.Close()
w.Close() // second teardown hits a dead fd
// after
var closed atomic.Bool
func closeOnce() error {
if !closed.CompareAndSwap(false, true) {
return nil
}
return w.Close()
} Defensive patterns
Strategy: try-catch
Try / catch
if err := w.Close(); err != nil {
if errors.Is(err, os.ErrClosed) || strings.Contains(err.Error(), "already closed") {
return nil
}
return err
} Prevention
- Single owner for shutdown sequencing; never invoke Close from two paths
- Wrap Close with sync.Once or an atomic flag in the caller
- Use the engine's Stop/netbird down rather than ad-hoc close calls
When it happens
Trigger: Calling Close() twice (second close hits a dead fd), the TUN device removed underneath the process, or a userspace/netstack device whose Close returns an error; the netstack-enabled branch returns right after this step.
Common situations: Duplicate shutdown paths (engine Stop plus a signal handler); test harnesses closing the iface directly; running netbird down twice; rebuilding the daemon without a prior down.
Related errors
- ICEBind has not been initialized yet
- received error "%w" while configuring interface %s with port
- received error "%w" while updating peer on interface %s with
- get peer: %w
- error removing peer %s from interface %s: %w
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/8ffdf6fe6df98bdc.
Report an issue: GitHub.