Files
nebula/e2e/info_api_test.go
T
Matt RichardsonandClaude Fable 5.1 3fb29d6444 Let a group share the info API too, and serve it on windows
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>
2026-10-21 12:26:57 -04:00

119 lines
4.6 KiB
Go

//go:build e2e_testing
package e2e
import (
"context"
"encoding/json"
"net/http"
"net/netip"
"path/filepath"
"runtime"
"testing"
"time"
"github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/cert_test"
"github.com/slackhq/nebula/e2e/router"
"github.com/slackhq/nebula/localapi"
"github.com/slackhq/nebula/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestIdentityOverTheInfoAPI is the info API's reason to exist, end to end: a program on
// this host has a connection arriving over the overlay and asks who is behind its
// source address, and the answer is the identity in the certificate the tunnel was
// established with.
func TestIdentityOverTheInfoAPI(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("the test's HTTP helpers dial unix sockets; on windows the info API is a named pipe")
}
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
myControl, myVpnIpNet, _, _ := newSimpleServer(cert.Version2, ca, caKey, "me", "10.128.0.1/24", nil)
theirControl, theirVpnIpNet, theirUdpAddr, _ := newSimpleServer(cert.Version2, ca, caKey, "them", "10.128.0.2/24", nil)
myControl.InjectLightHouseAddr(theirVpnIpNet[0].Addr(), theirUdpAddr)
r := router.NewR(t, myControl, theirControl)
defer r.RenderFlow()
myControl.Start()
theirControl.Start()
t.Cleanup(func() {
myControl.Stop()
theirControl.Stop()
})
// Before the tunnel is up their address is unknown to us.
path := infoSocket(t)
info := localapi.NewInfoServer(NewTestLogger(), myControl.Commands())
require.NoError(t, info.Rebind(context.Background(), path, localapi.Access{Mode: 0600}))
t.Cleanup(info.Stop)
status, body := test.HTTPGet(t, path, "/v1/identity?address="+theirVpnIpNet[0].Addr().String())
assert.Equal(t, http.StatusNotFound, status)
assert.JSONEq(t, `{"error":"No active tunnel for vpn addr: 10.128.0.2"}`, body)
// Only we know how to reach them, so the first packet has to be ours; once the tunnel
// is up both directions work and assertTunnel can check it.
myControl.InjectTunPacket(BuildTunUDPPacket(theirVpnIpNet[0].Addr(), 80, myVpnIpNet[0].Addr(), 80, []byte("Hi from me")))
p := r.RouteForAllUntilTxTun(theirControl)
assertUdpPacket(t, []byte("Hi from me"), p, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), 80, 80)
assertTunnel(t, myVpnIpNet[0].Addr(), theirVpnIpNet[0].Addr(), myControl, theirControl, r)
type identity struct {
Name string `json:"name"`
VpnAddrs []netip.Addr `json:"vpnAddrs"`
Networks []netip.Prefix `json:"networks"`
Groups []string `json:"groups"`
Issuer string `json:"issuer"`
}
// A connection's RemoteAddr, port and all, resolves to the peer.
status, body = test.HTTPGet(t, path, "/v1/identity?address="+netip.AddrPortFrom(theirVpnIpNet[0].Addr(), 51234).String())
require.Equal(t, http.StatusOK, status, body)
var them identity
require.NoError(t, json.Unmarshal([]byte(body), &them))
assert.Equal(t, "them", them.Name)
assert.Equal(t, []netip.Addr{theirVpnIpNet[0].Addr()}, them.VpnAddrs)
assert.Equal(t, theirVpnIpNet, them.Networks)
assert.Equal(t, []string{}, them.Groups)
caFP, err := ca.Fingerprint()
require.NoError(t, err)
assert.Equal(t, caFP, them.Issuer)
// No address is a caller that forgot the parameter, and is refused rather than handed
// this node's identity to authorize against; whoami is the command for that.
status, body = test.HTTPGet(t, path, "/v1/identity")
assert.Equal(t, http.StatusBadRequest, status, body)
assert.Contains(t, body, "whoami")
status, body = test.HTTPGet(t, path, "/v1/whoami")
require.Equal(t, http.StatusOK, status, body)
var me identity
require.NoError(t, json.Unmarshal([]byte(body), &me))
assert.Equal(t, "me", me.Name)
assert.Equal(t, []netip.Addr{myVpnIpNet[0].Addr()}, me.VpnAddrs)
// An empty address is a caller's bug, and is told so rather than handed our identity.
status, body = test.HTTPGet(t, path, "/v1/identity?address=")
assert.Equal(t, http.StatusBadRequest, status)
assert.JSONEq(t, `{"error":"The provided vpn addr could not be parsed: "}`, body)
// Nothing else nebula serves is reachable here.
status, body = test.HTTPGet(t, path, "/v1/list-hostmap")
assert.Equal(t, http.StatusForbidden, status)
assert.Contains(t, body, "identity lookups only")
r.RenderHostmaps("Final hostmaps", myControl, theirControl)
}
// infoSocket returns a short socket path in a private directory, one level below it
// so that listen has a directory of its own to create.
func infoSocket(t *testing.T) string {
t.Helper()
return filepath.Join(test.ShortTempDir(t), "n", "info.sock")
}