| index b4ab7b8e8..5100d6bf4 100644 |
| --- a/internal/unixdial/unixdial_test.go |
| +++ b/internal/unixdial/unixdial_test.go |
| @@ -3,6 +3,7 @@ package unixdial |
| import ( |
| "context" |
| "errors" |
| + "fmt" |
| "net" |
| "os" |
| "path/filepath" |
| @@ -40,14 +41,27 @@ func listen(t *testing.T, path string) net.Listener { |
| return ln |
| } |
| |
| -func TestCheckedAcceptsTheListenerItDialled(t *testing.T) { |
| - path := short(t, "app.sock") |
| - listen(t, path) |
| - c, err := Checked(context.Background(), &net.Dialer{}, path) |
| - if err != nil { |
| - t.Fatalf("Checked: %v", err) |
| +// TestCheckedAcceptsAListenerBoundAtTheDialledPath pins acceptance across a |
| +// few ordinary path shapes. It is not testing anything Checked branches on |
| +// by file name: the only path-based refusal is the control directory |
| +// (codex R5-1, covered by TestCheckedRefusesTheControlEndpointBeforeDialling |
| +// and the package doc), and the peer check consults no credentials by |
| +// construction — Checked never looks at SO_PEERCRED, so there is nothing |
| +// about "who created the listener" for a test to exercise (codex R4-1, |
| +// also covered by the package doc). These rows exist only to show the |
| +// ordinary-acceptance path isn't an accident of one particular name. |
| +func TestCheckedAcceptsAListenerBoundAtTheDialledPath(t *testing.T) { |
| + for _, name := range []string{"app.sock", "apps.sock", "example.sock"} { |
| + t.Run(name, func(t *testing.T) { |
| + path := short(t, name) |
| + listen(t, path) |
| + c, err := Checked(context.Background(), &net.Dialer{}, path) |
| + if err != nil { |
| + t.Fatalf("Checked: %v", err) |
| + } |
| + c.Close() |
| + }) |
| } |
| - c.Close() |
| } |
| |
| func TestCheckedRefusesASymlinkToAnotherListener(t *testing.T) { |
| @@ -75,29 +89,43 @@ func TestCheckedRefusesTheControlEndpointBeforeDialling(t *testing.T) { |
| } |
| } |
| |
| -func TestCheckedAcceptsAListenerCreatedByAnotherProcessShape(t *testing.T) { |
| - // The carlos-app@.socket shape: the listener's creator is not the |
| - // process that serves it. Credentials are never consulted, only the |
| - // bound path, so this passes as long as the path matches. |
| - path := short(t, "apps.sock") |
| - listen(t, path) |
| - c, err := Checked(context.Background(), &net.Dialer{}, path) |
| - if err != nil { |
| - t.Fatalf("Checked: %v", err) |
| +// TestCheckedRefusesAnAbstractPeer dials an abstract-namespace listener |
| +// (name[0] == '@', no filesystem path, so no symlink attack even applies) |
| +// directly by its abstract name. An abstract name never equals a cleaned |
| +// filesystem path, and verifyPeer refuses any peer whose bound name is |
| +// abstract outright rather than comparing it — abstract sockets carry no |
| +// filesystem permissions, so an abstract peer is never an acceptable |
| +// upstream. |
| +func TestCheckedRefusesAnAbstractPeer(t *testing.T) { |
| + name := fmt.Sprintf("@carlos-unixdial-test-%d", os.Getpid()) |
| + listen(t, name) |
| + c, err := Checked(context.Background(), &net.Dialer{}, name) |
| + if c != nil { |
| + c.Close() |
| + } |
| + if !errors.Is(err, ErrPeerMismatch) { |
| + t.Fatalf("err = %v, want ErrPeerMismatch", err) |
| } |
| - c.Close() |
| } |
| |
| -func TestCheckedAcceptsACustomPathOutsideRunCarlos(t *testing.T) { |
| - // codex R5-1: an operator's --socket /run/titogo/example.sock keeps |
| - // working; only the control endpoint is refused by path. |
| - path := short(t, "example.sock") |
| - listen(t, path) |
| - c, err := Checked(context.Background(), &net.Dialer{}, path) |
| - if err != nil { |
| - t.Fatalf("Checked: %v", err) |
| +// nameOnlyConn implements just enough of net.Conn for verifyPeer, which |
| +// only ever calls RemoteAddr on the connection it is checking. |
| +type nameOnlyConn struct { |
| + net.Conn |
| + remote net.Addr |
| +} |
| + |
| +func (c nameOnlyConn) RemoteAddr() net.Addr { return c.remote } |
| + |
| +// TestVerifyPeerRefusesAnEmptyNamePeer covers the unnamed-peer branch |
| +// directly: an unnamed unix socket (as from socketpair, or a client's own |
| +// unconnected end) reports an empty Name from getpeername, which must |
| +// never be treated as matching any dialled path. |
| +func TestVerifyPeerRefusesAnEmptyNamePeer(t *testing.T) { |
| + conn := nameOnlyConn{remote: &net.UnixAddr{Net: "unix", Name: ""}} |
| + if err := verifyPeer(conn, "/some/dialled/path.sock"); !errors.Is(err, ErrPeerMismatch) { |
| + t.Fatalf("verifyPeer = %v, want ErrPeerMismatch", err) |
| } |
| - c.Close() |
| } |
| |
| func TestCheckedPassesConnectErrorsThrough(t *testing.T) { |