{"record":{"id":"8ffdf6fe6df98bdc","repo":"netbirdio/netbird","slug":"failed-to-close-wireguard-interface-s-w","errorCode":null,"errorMessage":"failed to close wireguard interface %s: %w","messagePattern":"failed to close wireguard interface (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/iface/iface.go","lineNumber":230,"sourceCode":"func (w *WGIface) Close() error {\n\tw.mu.Lock()\n\n\tvar result *multierror.Error\n\n\tif err := w.wgProxyFactory.Free(); err != nil {\n\t\tresult = multierror.Append(result, fmt.Errorf(\"failed to free WireGuard proxy: %w\", err))\n\t}\n\n\t// Release w.mu before calling w.tun.Close(): the underlying\n\t// wireguard-go device.Close() waits for its send/receive goroutines\n\t// to drain. Some of those goroutines re-enter WGIface methods that\n\t// take w.mu (e.g. the packet filter DNS hook calls GetDevice()), so\n\t// holding the mutex here would deadlock the shutdown path.\n\ttun := w.tun\n\tw.mu.Unlock()\n\n\tif err := tun.Close(); err != nil {\n\t\tresult = multierror.Append(result, fmt.Errorf(\"failed to close wireguard interface %s: %w\", w.Name(), err))\n\t}\n\n\tif nbnetstack.IsEnabled() {\n\t\treturn errors.FormatErrorOrNil(result)\n\t}\n\n\tif err := w.waitUntilRemoved(); err != nil {\n\t\tlog.Warnf(\"failed to remove WireGuard interface %s: %v\", w.Name(), err)\n\t\tif err := w.Destroy(); err != nil {\n\t\t\tresult = multierror.Append(result, fmt.Errorf(\"failed to remove WireGuard interface %s: %w\", w.Name(), err))\n\t\t\treturn errors.FormatErrorOrNil(result)\n\t\t}\n\t\tlog.Infof(\"interface %s successfully removed\", w.Name())\n\t}\n\n\treturn errors.FormatErrorOrNil(result)\n}\n","sourceCodeStart":212,"sourceCodeEnd":248,"githubUrl":"https://github.com/netbirdio/netbird/blob/93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c/client/iface/iface.go#L212-L248","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nw.Close()\nw.Close() // second teardown hits a dead fd\n\n// after\nvar closed atomic.Bool\n\nfunc closeOnce() error {\n    if !closed.CompareAndSwap(false, true) {\n        return nil\n    }\n    return w.Close()\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := w.Close(); err != nil {\n    if errors.Is(err, os.ErrClosed) || strings.Contains(err.Error(), \"already closed\") {\n        return nil\n    }\n    return err\n}","preventionTips":["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"],"tags":["go","netbird","tun","shutdown","wireguard"],"backgroundTag":null,"analyzedSha":"93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c","analyzedAt":"2026-08-16T03:09:19.136Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}