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:noescape

View on GitHub (pinned to dd8f660c0a)

Solutions

  1. Ensure Close() is called only once per tun instance (use sync.Once)
  2. Check for other code paths closing the same fd/file
  3. Inspect the wrapped error for the underlying cause (EBADF usually means double close)
  4. 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

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


AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03). Data as JSON: /api/errors/ec7f79a544de3ea1. Report an issue: GitHub.