carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package unixdial
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "net"
8 "os"
9 "path/filepath"
10 "testing"
11)
12
13// short returns a socket path short enough for sockaddr_un under the
14// session's long TMPDIR (SDD lessons: run with TMPDIR=/tmp/g).
15func short(t *testing.T, name string) string {
16 t.Helper()
17 dir, err := os.MkdirTemp("", "ud")
18 if err != nil {
19 t.Fatal(err)
20 }
21 t.Cleanup(func() { os.RemoveAll(dir) })
22 return filepath.Join(dir, name)
23}
24
25func listen(t *testing.T, path string) net.Listener {
26 t.Helper()
27 ln, err := net.Listen("unix", path)
28 if err != nil {
29 t.Skipf("cannot bind a unix socket here (%v); run with TMPDIR=/tmp/g outside the sandbox", err)
30 }
31 t.Cleanup(func() { ln.Close() })
32 go func() {
33 for {
34 c, err := ln.Accept()
35 if err != nil {
36 return
37 }
38 c.Close()
39 }
40 }()
41 return ln
42}
43
44// TestCheckedAcceptsAListenerBoundAtTheDialledPath pins acceptance across a
45// few ordinary path shapes. It is not testing anything Checked branches on
46// by file name: the only path-based refusal is the control directory
47// (codex R5-1, covered by TestCheckedRefusesTheControlEndpointBeforeDialling
48// and the package doc), and the peer check consults no credentials by
49// construction — Checked never looks at SO_PEERCRED, so there is nothing
50// about "who created the listener" for a test to exercise (codex R4-1,
51// also covered by the package doc). These rows exist only to show the
52// ordinary-acceptance path isn't an accident of one particular name.
53func TestCheckedAcceptsAListenerBoundAtTheDialledPath(t *testing.T) {
54 for _, name := range []string{"app.sock", "apps.sock", "example.sock"} {
55 t.Run(name, func(t *testing.T) {
56 path := short(t, name)
57 listen(t, path)
58 c, err := Checked(context.Background(), &net.Dialer{}, path)
59 if err != nil {
60 t.Fatalf("Checked: %v", err)
61 }
62 c.Close()
63 })
64 }
65}
66
67func TestCheckedRefusesASymlinkToAnotherListener(t *testing.T) {
68 real := short(t, "other.sock")
69 listen(t, real)
70 link := short(t, "app.sock")
71 if err := os.Symlink(real, link); err != nil {
72 t.Fatal(err)
73 }
74 c, err := Checked(context.Background(), &net.Dialer{}, link)
75 if c != nil {
76 c.Close()
77 }
78 if !errors.Is(err, ErrPeerMismatch) {
79 t.Fatalf("err = %v, want ErrPeerMismatch", err)
80 }
81}
82
83func TestCheckedRefusesTheControlEndpointBeforeDialling(t *testing.T) {
84 // No listener exists; the refusal must come from the path check, not
85 // from a connect error.
86 _, err := Checked(context.Background(), &net.Dialer{}, "/run/carlos-control/control.sock")
87 if !errors.Is(err, ErrControlEndpoint) {
88 t.Fatalf("err = %v, want ErrControlEndpoint", err)
89 }
90}
91
92// TestCheckedRefusesAnAbstractPeer dials an abstract-namespace listener
93// (name[0] == '@', no filesystem path, so no symlink attack even applies)
94// directly by its abstract name. An abstract name never equals a cleaned
95// filesystem path, and verifyPeer refuses any peer whose bound name is
96// abstract outright rather than comparing it — abstract sockets carry no
97// filesystem permissions, so an abstract peer is never an acceptable
98// upstream.
99func TestCheckedRefusesAnAbstractPeer(t *testing.T) {
100 name := fmt.Sprintf("@carlos-unixdial-test-%d", os.Getpid())
101 listen(t, name)
102 c, err := Checked(context.Background(), &net.Dialer{}, name)
103 if c != nil {
104 c.Close()
105 }
106 if !errors.Is(err, ErrPeerMismatch) {
107 t.Fatalf("err = %v, want ErrPeerMismatch", err)
108 }
109}
110
111// nameOnlyConn implements just enough of net.Conn for verifyPeer, which
112// only ever calls RemoteAddr on the connection it is checking.
113type nameOnlyConn struct {
114 net.Conn
115 remote net.Addr
116}
117
118func (c nameOnlyConn) RemoteAddr() net.Addr { return c.remote }
119
120// TestVerifyPeerRefusesAnEmptyNamePeer covers the unnamed-peer branch
121// directly: an unnamed unix socket (as from socketpair, or a client's own
122// unconnected end) reports an empty Name from getpeername, which must
123// never be treated as matching any dialled path.
124func TestVerifyPeerRefusesAnEmptyNamePeer(t *testing.T) {
125 conn := nameOnlyConn{remote: &net.UnixAddr{Net: "unix", Name: ""}}
126 if err := verifyPeer(conn, "/some/dialled/path.sock"); !errors.Is(err, ErrPeerMismatch) {
127 t.Fatalf("verifyPeer = %v, want ErrPeerMismatch", err)
128 }
129}
130
131// TestVerifyPeerRequiresByteEqualBoundPath pins the review fix: the peer
132// name the kernel reports must equal the cleaned dialled path byte for
133// byte, not merely after being cleaned itself, and a peer name with a ".."
134// component is refused outright even when a naive clean-and-compare would
135// have accepted it.
136func TestVerifyPeerRequiresByteEqualBoundPath(t *testing.T) {
137 conn := nameOnlyConn{remote: &net.UnixAddr{Net: "unix", Name: "/x/../app.sock"}}
138 if err := verifyPeer(conn, "/app.sock"); !errors.Is(err, ErrPeerMismatch) {
139 t.Fatalf("verifyPeer(%q) = %v, want ErrPeerMismatch: a peer name with a .. component must never be accepted", "/x/../app.sock", err)
140 }
141
142 conn = nameOnlyConn{remote: &net.UnixAddr{Net: "unix", Name: "/app.sock"}}
143 if err := verifyPeer(conn, "/app.sock"); err != nil {
144 t.Fatalf("verifyPeer(%q) = %v, want nil: an exact byte-for-byte match must be accepted", "/app.sock", err)
145 }
146}
147
148func TestCheckedPassesConnectErrorsThrough(t *testing.T) {
149 path := short(t, "absent.sock")
150 _, err := Checked(context.Background(), &net.Dialer{}, path)
151 if err == nil || errors.Is(err, ErrPeerMismatch) || errors.Is(err, ErrControlEndpoint) {
152 t.Fatalf("err = %v, want the raw connect error (ENOENT) so retry logic still sees it", err)
153 }
154}
155