Skip to content

Commit 35e8df4

Browse files
committed
nano-init: bound the ingress handshake read
ingressMaxHandshake sizes the bufio.Reader that reads "CONNECT <port>", and the comment beside it says anything longer is a client that has misunderstood. It bounds nothing. A bufio.Reader's size limits one fill, while ReadString goes on growing a buffer of its own until it finds a newline, so a client that opens the ingress socket and never sends one is read until the deadline expires -- and the process accumulating it is PID 1 in the sandbox. Read with ReadSlice, which stops at the buffer and reports ErrBufferFull, and answer that refusal rather than dropping it, for the reason the malformed-handshake path already answers: a gateway that gets nothing back cannot tell a refusal from a sandbox that never started. The far side of this same handshake, dialSandbox in internal/sambox/ingress.go, already bounds its read with io.LimitReader. The added test reads a 1 MiB flood with no newline through handleIngress and holds it to ingressMaxHandshake; against the current code it reads all 1048576 bytes and answers nothing. The pipelined-bytes test covers what a bounded read must not break, since bytes the gateway sends in the same write as the handshake sit in the reader rather than the socket when the agent is dialled and are forwarded by hand. parseIngressConnect had no test of its own.
1 parent adb56e2 commit 35e8df4

2 files changed

Lines changed: 188 additions & 6 deletions

File tree

cmd/nano-init/ingress.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package main
1717
import (
1818
"bufio"
1919
"context"
20+
"errors"
2021
"fmt"
2122
"io"
2223
"log"
@@ -95,18 +96,24 @@ func handleIngress(ctx context.Context, conn net.Conn) error {
9596

9697
_ = conn.SetReadDeadline(time.Now().Add(ingressConnectTimeout))
9798
reader := bufio.NewReaderSize(conn, ingressMaxHandshake)
98-
line, err := reader.ReadString('\n')
99+
// ReadSlice rather than ReadString: ReadString grows a buffer of its own
100+
// until it finds a newline, so the size above would bound nothing, and a
101+
// client that never sends one could make this process -- PID 1 in the
102+
// sandbox -- accumulate for as long as the deadline allows. ReadSlice
103+
// stops at the buffer and says so. The far side of this handshake bounds
104+
// its read the same way; see dialSandbox in internal/sambox/ingress.go.
105+
line, err := reader.ReadSlice('\n')
99106
if err != nil {
107+
if errors.Is(err, bufio.ErrBufferFull) {
108+
return refuseIngress(conn, fmt.Errorf("the handshake is longer than %d bytes", ingressMaxHandshake))
109+
}
100110
return fmt.Errorf("read the ingress handshake: %w", err)
101111
}
102112
_ = conn.SetReadDeadline(time.Time{})
103113

104-
port, err := parseIngressConnect(line)
114+
port, err := parseIngressConnect(string(line))
105115
if err != nil {
106-
// Answered rather than dropped: a gateway that gets nothing back
107-
// cannot tell a refusal from a sandbox that never started.
108-
_, _ = io.WriteString(conn, "ERR "+err.Error()+"\n")
109-
return err
116+
return refuseIngress(conn, err)
110117
}
111118

112119
// The agent is in this namespace, which is the whole reason this hop
@@ -138,6 +145,15 @@ func handleIngress(ctx context.Context, conn net.Conn) error {
138145
return nil
139146
}
140147

148+
// refuseIngress tells the gateway why its handshake was not honoured.
149+
//
150+
// Answered rather than dropped: a gateway that gets nothing back cannot tell
151+
// a refusal from a sandbox that never started.
152+
func refuseIngress(conn net.Conn, err error) error {
153+
_, _ = io.WriteString(conn, "ERR "+err.Error()+"\n")
154+
return err
155+
}
156+
141157
// parseIngressConnect reads the one line the gateway sends first.
142158
func parseIngressConnect(line string) (int, error) {
143159
fields := strings.Fields(strings.TrimSpace(line))

cmd/nano-init/ingress_test.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@
1515
package main
1616

1717
import (
18+
"bufio"
19+
"bytes"
1820
"context"
21+
"io"
22+
"net"
1923
"os"
2024
"path/filepath"
25+
"strings"
2126
"syscall"
2227
"testing"
2328
"time"
@@ -69,3 +74,164 @@ func TestIngressSocketIsOwnerOnly(t *testing.T) {
6974
t.Error("serveIngress did not return after cancellation")
7075
}
7176
}
77+
78+
// floodConn is a client that opens the ingress socket and then never sends the
79+
// newline the handshake ends with. It counts what the handler reads, so a test
80+
// can hold the handler to the bound the package documents.
81+
type floodConn struct {
82+
read int
83+
limit int
84+
reply bytes.Buffer
85+
}
86+
87+
func (c *floodConn) Read(p []byte) (int, error) {
88+
if c.read >= c.limit {
89+
return 0, io.EOF
90+
}
91+
n := len(p)
92+
if remaining := c.limit - c.read; n > remaining {
93+
n = remaining
94+
}
95+
for i := range p[:n] {
96+
p[i] = 'A'
97+
}
98+
c.read += n
99+
return n, nil
100+
}
101+
102+
func (c *floodConn) Write(p []byte) (int, error) { return c.reply.Write(p) }
103+
func (c *floodConn) Close() error { return nil }
104+
func (c *floodConn) LocalAddr() net.Addr { return floodAddr{} }
105+
func (c *floodConn) RemoteAddr() net.Addr { return floodAddr{} }
106+
func (c *floodConn) SetDeadline(time.Time) error { return nil }
107+
func (c *floodConn) SetReadDeadline(time.Time) error { return nil }
108+
func (c *floodConn) SetWriteDeadline(time.Time) error { return nil }
109+
110+
type floodAddr struct{}
111+
112+
func (floodAddr) Network() string { return "flood" }
113+
func (floodAddr) String() string { return "flood" }
114+
115+
// TestHandleIngressBoundsTheHandshake holds the handshake read to the size the
116+
// package names. ingressMaxHandshake sizes a bufio.Reader, and that bounds only
117+
// what one fill holds: ReadString goes on growing a buffer of its own until it
118+
// finds a newline, so a client that sends none could make this process -- PID 1
119+
// in the sandbox -- accumulate for as long as the read deadline allows.
120+
func TestHandleIngressBoundsTheHandshake(t *testing.T) {
121+
const flood = 1 << 20
122+
conn := &floodConn{limit: flood}
123+
124+
err := handleIngress(context.Background(), conn)
125+
if err == nil {
126+
t.Fatal("handleIngress accepted a handshake with no newline, want an error")
127+
}
128+
if got := conn.read; got > ingressMaxHandshake {
129+
t.Errorf("handleIngress read %d bytes of a %d byte flood, want at most %d", got, flood, ingressMaxHandshake)
130+
}
131+
if answer := conn.reply.String(); !strings.HasPrefix(answer, "ERR ") {
132+
t.Errorf("handleIngress answered %q, want an ERR line: a gateway that gets nothing back cannot tell a refusal from a sandbox that never started", answer)
133+
}
134+
}
135+
136+
// TestHandleIngressRelaysPipelinedBytes covers what a bounded read must not
137+
// break. The gateway may send its first request bytes in the same write as the
138+
// handshake, and those are in the reader rather than the socket by the time the
139+
// agent is dialled, so they are forwarded by hand.
140+
func TestHandleIngressRelaysPipelinedBytes(t *testing.T) {
141+
const pipelined = "HELLO"
142+
143+
agent, err := net.Listen("tcp", "127.0.0.1:0")
144+
if err != nil {
145+
t.Fatalf("listen as the agent: %v", err)
146+
}
147+
defer func() { _ = agent.Close() }()
148+
_, port, err := net.SplitHostPort(agent.Addr().String())
149+
if err != nil {
150+
t.Fatalf("split the agent address: %v", err)
151+
}
152+
153+
delivered := make(chan string, 1)
154+
go func() {
155+
c, err := agent.Accept()
156+
if err != nil {
157+
delivered <- "accept: " + err.Error()
158+
return
159+
}
160+
defer func() { _ = c.Close() }()
161+
buf := make([]byte, len(pipelined))
162+
if _, err := io.ReadFull(c, buf); err != nil {
163+
delivered <- "read: " + err.Error()
164+
return
165+
}
166+
delivered <- string(buf)
167+
}()
168+
169+
client, server := net.Pipe()
170+
defer func() { _ = client.Close() }()
171+
done := make(chan error, 1)
172+
go func() { done <- handleIngress(context.Background(), server) }()
173+
go func() { _, _ = io.WriteString(client, "CONNECT "+port+"\n"+pipelined) }()
174+
175+
reply, err := bufio.NewReader(io.LimitReader(client, 128)).ReadString('\n')
176+
if err != nil {
177+
t.Fatalf("read the handshake answer: %v", err)
178+
}
179+
if got := strings.TrimSpace(reply); got != "OK" {
180+
t.Fatalf("the handshake answer is %q, want OK", got)
181+
}
182+
183+
select {
184+
case got := <-delivered:
185+
if got != pipelined {
186+
t.Errorf("the agent received %q, want %q", got, pipelined)
187+
}
188+
case <-time.After(5 * time.Second):
189+
t.Fatal("the agent never received the bytes pipelined behind the handshake")
190+
}
191+
192+
select {
193+
case <-done:
194+
case <-time.After(5 * time.Second):
195+
t.Error("handleIngress did not return once both ends were done")
196+
}
197+
}
198+
199+
// TestParseIngressConnect pins the one line the gateway sends first. Everything
200+
// past it is relayed verbatim, so this is where a malformed request has to stop.
201+
func TestParseIngressConnect(t *testing.T) {
202+
for _, tc := range []struct {
203+
name string
204+
line string
205+
want int
206+
}{
207+
{name: "port", line: "CONNECT 8080\n", want: 8080},
208+
{name: "lowercase verb", line: "connect 8080\n", want: 8080},
209+
{name: "extra spaces", line: " CONNECT 8080 \n", want: 8080},
210+
{name: "lowest port", line: "CONNECT 1\n", want: 1},
211+
{name: "highest port", line: "CONNECT 65535\n", want: 65535},
212+
{name: "port zero", line: "CONNECT 0\n"},
213+
{name: "above the port range", line: "CONNECT 65536\n"},
214+
{name: "negative", line: "CONNECT -1\n"},
215+
{name: "not a number", line: "CONNECT http\n"},
216+
{name: "wrong verb", line: "GET 8080\n"},
217+
{name: "no port", line: "CONNECT\n"},
218+
{name: "trailing junk", line: "CONNECT 8080 now\n"},
219+
{name: "empty", line: "\n"},
220+
} {
221+
t.Run(tc.name, func(t *testing.T) {
222+
got, err := parseIngressConnect(tc.line)
223+
if tc.want == 0 {
224+
if err == nil {
225+
t.Fatalf("parseIngressConnect(%q) = %d, want an error", tc.line, got)
226+
}
227+
return
228+
}
229+
if err != nil {
230+
t.Fatalf("parseIngressConnect(%q) returned %v, want %d", tc.line, err, tc.want)
231+
}
232+
if got != tc.want {
233+
t.Errorf("parseIngressConnect(%q) = %d, want %d", tc.line, got, tc.want)
234+
}
235+
})
236+
}
237+
}

0 commit comments

Comments
 (0)