The check that the control socket and the info API are not configured
on one path compared the strings, so a doubled or trailing separator, or
a case difference on windows where pipe names are case-insensitive, got
past it and the second bind failed with a message about another nebula.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A program reading the info API opens the pipe by name and has no owner
check of its own, so for a name any local user could create first,
whoever did would be answering its queries. nebula refuses to serve a
held name and logs it, but the program cannot tell. The daemon now warns
at bind when the configured name is not under
\\.\pipe\ProtectedPrefix\Administrators\, and the example config and
CHANGELOG say to put it there.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A program that verifies certificates against its own CAs, as
nebula-auth-request does, needs the certificate itself rather than the
parsed identity, and it asked the ssh debug server for it. The info API
now serves print-cert, so it can ask there instead.
- print-cert declares Identity: it reads a certificate from the hostmap or our own, and nothing operational
- -address names the vpn addr, with a port accepted and stripped and 4in6 unmapped, as identity takes it; the operand stays, and giving both is refused
- an address of our own answers with our own certificate, and a tunnel without one is not found, a 404 on the API
- with no address the certificate is the one whoami reports, the v2 when both versions are loaded, not the one handshakes start with
- GET /v1/print-cert?address=<addr>&raw=true answers {"pem": ...}; example config, CHANGELOG, surface test, goldens and an e2e check
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- the rationale for Wire's first attempt and its reload callback being the same call is one sentence, not five
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The control socket's fold made Group a value and gave the two 0600
constants one name; the info API's config reader and its tests catch up.
- defaultInfoMode goes for ownerOnlyMode; a group is present when its ID is
- the config test compares the whole Access with ownerOnly()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The endpoint is one type under both servers, so the info API's copies of
the rebind, failed-rebind and cancellation tests ran the control socket's
code a second time, and in-place access changes were pinned twice more.
- info API: the three endpoint tests, the mode test and the wire test's mode step go
- the control socket's group hand-over test stands for both servers
- registry: one Resolve test with both the typed and the worded assertions, in registry_test
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
It is served there now, on a pipe, so nothing set the flag any more. It
was also a plain bool written by the first reconcile and by the reload
callback, which was the review's point about it.
- infoSupported, errInfoUnsupported, the Rebind check and the reload
branch that warned once go
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The info API is for other programs on the host, and on unix the only
setting between "the user nebula runs as" and "everyone" was a manual
chgrp that the next bind undid; on windows it was not served at all,
because which accounts a pipe admits had not been decided. The control
socket's `group` decides it, and the info API takes the same key.
- `local_api.info.group`: the socket is handed to the group at 0660
unless `socket_mode` says otherwise, and a mode that gives a named
group nothing is refused with the mode that would
- windows: served on a named pipe, the group an ACE on it as on the
control pipe; the e2e test's helpers dial unix sockets, so it still
skips there
- a changed group or mode on a reload is applied to the socket in place
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Build slackhq/nebula#1821 ("who is this vpn address?" over a local
HTTP+JSON socket) on the command registry, so the lookup is one command
on every surface and the socket is one more local API.
identity
- `identity [-address <vpn addr>[:port]]`: no address reports this node;
with one, this node or the peer behind an established tunnel
- Certificate fields only: name, vpn addresses, networks, unsafe
networks, groups, fingerprint, issuer, validity, cert version; a peer's
fingerprint is the one nebula computed when it verified the cert
- Port accepted and stripped, 4in6 unmapped, so a RemoteAddr passes
straight through
- No tunnel is a commands.NotFound; an empty -address is refused
- Lighthouse TXT lookup shares the owner and peer lookups, keeps answering
with the initiating certificate, and no longer panics without a default cert
- Served on ssh and nebula ctl too
Info API
- Off until `local_api.info.enabled` with `socket` and optional quoted
octal `socket_mode` (default "0600"); nothing derived
- Serves commands declaring Command.Identity plus help; others 403 naming
their surfaces. TestInfoAPISurface pins the set to identity
- GET /v1 is the schema; GET /v1/<cmd>?<flag>=<value> runs one; body is
the -json rendering via commands.Render; commands.PresentationFormat
reads json/pretty from the query the way a line is read
- 400 commands.ParamError, 403 commands.WithheldError (what Resolve now
returns for a withheld name, so help and dispatch type-check it too),
404 NotFound or unknown, 500 Run error, refusal with no reason, or
recovered panic with no detail; no redirects; every miss is JSON
- listen takes a mode: bind under umask 0077 then chmod; "0660" plus a
searchable dir opens it to a group
- The chmod, at bind and on a mode-only reload, goes through chmodSocket,
which never follows a link: a user who can write the socket's directory
could otherwise swap the node for a link and have a root daemon put
0660 or 0666 on any file. Only a socket we own is chmoded. On linux the
node is held open O_PATH|O_NOFOLLOW, checked by fstat, and chmoded
through /proc/self/fd, so the check and the chmod land on one inode;
fchmodat2 is the fallback without /proc. On darwin and the BSDs it is
an Lstat check and fchmodat(AT_SYMLINK_NOFOLLOW). clearStale Lstats
too, so a dangling link is refused rather than followed by bind on
darwin
- One endpoint type owns bind under mutex, bind-before-release move,
context watch and stop/unlink; each server supplies what it serves
- Reloadable through the same reconcile shape as the control socket:
mode applied in place, unparsable section keeps what runs, control
socket path refused by name
- Wire returns Endpoints{Control, Info} with one Stop
- Windows unsupported for now (a pipe has no mode); warned once
Tests: identity lookups and shapes; API filter, status codes, panic
recovery, socket mode, rebind, cancellation; config parsing incl. bare
0660; Wire transitions; e2e of two nebulas with the info API resolving
the peer by address and port, through shared test.HTTPDo/HTTPGet helpers.
Goldens gain identity.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add support for accessing the previously ssh-only debug commands via
`nebula ctl <command>.`
- unix socket (named pipe on windows) for communication
- sockets scoped to the user + an optional group
- in cmd/nebula and cmd/nebula-service, but not nebula.Main
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
unlockedSetV4/V6 truncated the reported list to MaxRemotes and only then
ran the allow list check. A host reporting more than MaxRemotes addrs
(docker bridges, temporary v6 addrs, etc) could have every allowed addr
discarded if the denied ones sorted first. Filter the full list and stop
once MaxRemotes allowed addrs are collected. This affects lighthouses
processing HostUpdateNotification and clients processing HostQueryReply.
Report ssh command failures to the surface (#1959)
The ssh debug commands told the user when something went wrong but
never told the surface that ran them, so `ssh host cmd` always exited 0
and a few failures were not reported at all. A failed command now
returns its error, a failure the user has already heard about is a
commands.Failure, and the ssh exec path exits 1 for either. The command
bodies also lose their ssh prefixes and the duplication they carried
over from ssh.go; both goldens pin that nothing else changed.
- Dispatch returns the callback's error, or shlex's; the ssh session
logs it at debug, and not at all for a Failure the user already saw
- commands.Fail writes a message and returns it as a Failure in one step;
an unknown command, `help` for a command that is not there, a failed
reload and an unbalanced quote are failures
- ssh exec exit-status is 1 for a command, flag or failure error, 0
otherwise; the e2e golden records the exit status for every case
- Clone rebinds help to the copy, so an ssh session's help lists logout
and `help logout` and `logout -h` answer; sshd.LogoutCommand is the
session's logout, exported for tests
- "did not understand" names the first word only, the one looked up;
DispatchArgs takes just the tokens
- log-format writes its refusal to the user, as log-level does
- print-relays emits the relay error as a string and omits the key when
there is none, where it printed {} or "Error":null
- list-hostmap and list-lighthouse-addrmap -json return the encode
error; print-cert reports a certificate that will not marshal
- a flags type that is not the one registered is an internal error, not
a silent success
- command functions and flag types are <name>Command and <name>Flags;
parseVpnAddr and listHostMapFlagSet replace the repeated parse block
and flag set; close-tunnel sends through sendCloseTunnel
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Migrate commands from ssh into a transport neutral package
- add golden tests validating that output remains unchanged
- move commands to commands.go
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
gvisor's arm64 race-build compare-and-swap (pkg/sync/race_arm64.s)
loaded the new value into the register meant for the old one (R1
instead of R2), so it never succeeded. Sleepers could not commit to
parking and spun: under -race on arm64 every idle service netstack
busied about 14 cores, and TestService's static host lookup of
localhost missed its 250ms timeout and stalled for the 30s static_map
cadence. Fixed in gvisor 1b2946c4fa.
Go 1.27.2 is now the minimum because gvisor's go branch requires it.
golangci-lint is bumped to v2.14, the first release that can read
go1.27.2 export data.
Tailscale with an exit node treats every interface address prefix as an
attached LAN and either claims it or excludes it, and configd then rewrites
the matching route away from nebula's utun. Interfaces holding a single
address are skipped, so assign /32 and /128 and add the network route
ourselves for both families.
* examples: set socket buffers and raise tun tx_queue
* examples: use the default tcp max window for socket buffers
* examples: note buffers are linux only, clarify tx_queue default
* tun: default tx_queue to 5000
On a domain-joined Windows host the DNS Client registers every adapter's
addresses under the host name in AD DNS when "Register this connection's
addresses in DNS" is on, which is the default for a new adapter. When the
Nebula adapter registers, LAN clients that are not on the overlay resolve
the host name to its overlay IP and RDP, SMB and PowerShell Remoting to it
fail intermittently, with nothing pointing at the tunnel as the cause.
Windows registers on address assignment, periodically, and on every
ipconfig /registerdns.
Right after the wintun adapter is created and before Activate assigns any
address, write RegistrationEnabled=0, DisableDynamicUpdate=1 and
MaxNumberOfAddressesToRegister=0 under both the Tcpip and Tcpip6
Parameters\Interfaces\{GUID} keys. The adapter checkbox alone has proven
insufficient in practice, as has disabling dynamic updates alone, so all
three are set, matching what Tailscale and NetBird do. A failure to write
the keys is logged as a warning and does not fail adapter creation.
StartHandshake returned a hostinfo with no RemoteList; handleOutbound attached
one lazily on its first tick. Anything that touched the pending host before
then, notably create-tunnel -address, dereferenced nil and took the daemon
down. StartHandshake now attaches the lighthouse's list as it builds the
hostinfo. To keep the surrounding locking sound, a lighthouse now resolves a
punch target before taking the querier's RemoteList lock, and closeTunnel
deletes the host and evicts its RemoteList under a single hostmap write lock so
a completing handshake cannot land in between. The lock order, now documented
on the handshake manager mutex, is hostmap, then handshake manager, then
lighthouse, then RemoteList; no RemoteList lock is ever held across a hostmap
lookup.
newTunFromFd was a stub on darwin, so an embedder handed a utun by the OS had
no way to use it. A darwin network extension is given exactly that:
NEPacketTunnelProvider creates the device and applies addressing, mtu and
routes from its network settings, and a sandboxed extension cannot change them
afterwards. Adopting that fd means everything about moving packets is shared
with newTun and only the setup differs, so the tun tracks whether the host owns
it and leaves the system routing table and interface config alone when it does.
Co-authored-by: Nate Brown <nbrown.us@gmail.com>