slackhq/nebula · warning
error closing tun file: %w
Error message
error closing tun file: %w
What it means
Close() closes the os.File wrapping the tun file descriptor and reports any error returned by f.Close(), wrapped with this message. A failing close can indicate the fd was already closed or an underlying I/O problem flushing state. The code still force-closes the raw fd as a safety net.
Source
Thrown at overlay/tun_openbsd.go:116
err = t.reload(c, true)
if err != nil {
return nil, err
}
c.RegisterReloadCallback(func(c *config.C) {
err := t.reload(c, false)
if err != nil {
util.LogWithContextIfNeeded("failed to reload tun device", err, t.l)
}
})
return t, nil
}
func (t *tun) Close() error {
if t.f != nil {
if err := t.f.Close(); err != nil {
return fmt.Errorf("error closing tun file: %w", err)
}
// t.f.Close should have handled it for us but let's be extra sure
_ = unix.Close(t.fd)
}
return nil
}
// tunWritev and tunReadv are linkname'd to x/sys/unix's libc-routed writev/readv stubs so the
// calls go through libc's pinned trampoline. OpenBSD's pinsyscall protection rejects a raw
// syscall.Syscall(SYS_WRITEV/SYS_READV, ...) because it doesn't originate from a libc-pinned
// address, so we can't use the syscall.Syscall pattern that freebsd / netbsd use. We pull the
// low-level stubs instead of calling unix.Writev/unix.Readv because those take [][]byte and rebuild
// the []Iovec every call, which heap-allocates the header; linkname'ing the stubs lets us hand them
// our own stack-allocated iovecs. See golang/go#78049.
//go:linkname tunWritev golang.org/x/sys/unix.writev
//go:noescapeView on GitHub (pinned to dd8f660c0a)
Solutions
- Ensure Close() is called only once per tun instance (use sync.Once)
- Check for other code paths closing the same fd/file
- Inspect the wrapped error for the underlying cause (EBADF usually means double close)
- Restart the process if the tun device is left in a bad state
Example fix
// before
t.Close()
t.Close() // second call errors
// after
var closeOnce sync.Once
closeOnce.Do(func() { _ = t.Close() }) Defensive patterns
Strategy: try-catch
Try / catch
if err := t.Close(); err != nil {
var errno syscall.Errno
if errors.As(err, &errno) && errno == syscall.EBADF {
// already closed; safe to ignore
} else {
return fmt.Errorf("tun close: %w", err)
}
} Prevention
- Close each tun exactly once (sync.Once)
- Never close the tun fd from other goroutines
- Log and continue on EBADF during shutdown
When it happens
Trigger: Calling tun.Close() (directly or during shutdown) when the underlying file/fd is already invalid or was closed elsewhere, or the kernel reports an error on close.
Common situations: Double-close during shutdown ordering bugs; another goroutine closing the tun file; fd corruption after a signal-triggered teardown race.
Related errors
- error closing tun file: %w
- newTunFromFd not supported in FreeBSD
- failed to set tun device as nonblocking: %w
- failed to create shutdown pipe: %w
- failed to set the tun fd to non-blocking mode: %w
AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03).
Data as JSON: /api/errors/ec7f79a544de3ea1.
Report an issue: GitHub.