mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 11:36:52 +01:00
Add a local control socket and nebula ctl (#1975)
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>
This commit is contained in:
1 parent
9a5bdb508e
commit
0f767ce529
54 files changed
+7581
-925
No files matched your search
@@ -0,0 +1,102 @@
|
||||
package localapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestReadFrameLimitIsOnTheBody pins the framing rule at its edge: the limit bounds the
|
||||
// JSON body, so the newline that terminates the frame must not count against it.
|
||||
func TestReadFrameLimitIsOnTheBody(t *testing.T) {
|
||||
// A JSON string whose encoded form is exactly n bytes, with or without a terminator.
|
||||
body := func(n int) string {
|
||||
return fmt.Sprintf(`"%s"`, strings.Repeat("a", n-2))
|
||||
}
|
||||
|
||||
var v string
|
||||
|
||||
// Exactly at the limit, terminated: accepted.
|
||||
v = ""
|
||||
require.NoError(t, readFrame(bytes.NewReader([]byte(body(64)+"\n")), 64, "request", &v, true))
|
||||
assert.Len(t, v, 62)
|
||||
|
||||
// Exactly at the limit, EOF instead of a newline: still accepted.
|
||||
v = ""
|
||||
require.NoError(t, readFrame(bytes.NewReader([]byte(body(64))), 64, "request", &v, true))
|
||||
assert.Len(t, v, 62)
|
||||
|
||||
// One byte over: refused as oversized, not as a parse failure.
|
||||
err := readFrame(bytes.NewReader([]byte(body(65)+"\n")), 64, "request", &v, true)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "exceeded 64 bytes")
|
||||
}
|
||||
|
||||
// stallingReader hands out its bytes and then fails with err, the shape of a peer that
|
||||
// sends part of a frame and then stops, until the deadline on the connection lands.
|
||||
type stallingReader struct {
|
||||
data []byte
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *stallingReader) Read(p []byte) (int, error) {
|
||||
if len(r.data) == 0 {
|
||||
return 0, r.err
|
||||
}
|
||||
n := copy(p, r.data)
|
||||
r.data = r.data[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
type timeoutError struct{}
|
||||
|
||||
func (timeoutError) Error() string { return "i/o timeout" }
|
||||
func (timeoutError) Timeout() bool { return true }
|
||||
func (timeoutError) Temporary() bool { return true }
|
||||
|
||||
// TestReadFramePartialLineThenReadErrorIsTheError: a read that fails after part of a
|
||||
// frame has arrived is a failed read, and the caller sees that error. The old behaviour
|
||||
// treated the fragment as the whole frame and reported a parse failure, so a client
|
||||
// that stalled mid request was told its JSON was malformed, and a daemon that stalled
|
||||
// mid response hid the timeout the client's error path looks for.
|
||||
func TestReadFramePartialLineThenReadErrorIsTheError(t *testing.T) {
|
||||
var v map[string]any
|
||||
|
||||
err := readFrame(&stallingReader{data: []byte(`{"command": "hel`), err: timeoutError{}}, 1024, "request", &v, true)
|
||||
require.Error(t, err)
|
||||
var ne net.Error
|
||||
require.ErrorAs(t, err, &ne, "the timeout is still visible through the wrapping: %v", err)
|
||||
assert.True(t, ne.Timeout())
|
||||
assert.NotContains(t, err.Error(), "parse", "a stalled peer is not a parse failure")
|
||||
|
||||
// EOF after an unterminated line is the one exception: a final line with no newline
|
||||
// is still a frame.
|
||||
v = nil
|
||||
require.NoError(t, readFrame(&stallingReader{data: []byte(`{"command": "help"}`), err: io.EOF}, 1024, "request", &v, true))
|
||||
assert.Equal(t, "help", v["command"])
|
||||
|
||||
// And an unterminated line that is EOF only because the limit cut it off is still
|
||||
// reported as oversized.
|
||||
err = readFrame(bytes.NewReader([]byte(strings.Repeat("a", 70)+"\n")), 64, "request", &v, true)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "exceeded 64 bytes")
|
||||
}
|
||||
|
||||
// TestReadFrameUnknownFieldIsRefusedWhenStrict: the daemon refuses a request field it
|
||||
// does not know, as it refuses an unknown parameter, so a misspelling is reported rather
|
||||
// than ignored; the client, reading leniently, passes over a field a newer daemon sends.
|
||||
func TestReadFrameUnknownFieldIsRefusedWhenStrict(t *testing.T) {
|
||||
var req Request
|
||||
err := readFrame(bytes.NewReader([]byte(`{"version":1,"comand":"help"}`+"\n")), 1024, "request", &req, true)
|
||||
require.ErrorContains(t, err, `unknown field "comand"`)
|
||||
|
||||
var res Response
|
||||
require.NoError(t, readFrame(bytes.NewReader([]byte(`{"version":1,"later":true}`+"\n")), 1024, "response", &res, false))
|
||||
assert.Equal(t, 1, res.Version)
|
||||
}
|
||||
Reference in new issue
Block a user