Files
nebula/localapi/protocol_test.go
T
Matt RichardsonandClaude Fable 5.1 0f767ce529 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>
2026-10-21 11:16:00 -04:00

103 lines
3.8 KiB
Go

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)
}