-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpipetransport_windows_test.go
More file actions
117 lines (106 loc) · 4.13 KB
/
Copy pathpipetransport_windows_test.go
File metadata and controls
117 lines (106 loc) · 4.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
//go:build windows
package main
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/Microsoft/go-winio"
)
// TestHonorListenPipeWindows: on Windows the transport is real, so the flag passes
// through unchanged.
func TestHonorListenPipeWindows(t *testing.T) {
if !honorListenPipe(true) {
t.Error("honorListenPipe(true) = false, want true on Windows")
}
if honorListenPipe(false) {
t.Error("honorListenPipe(false) = true, want false")
}
}
// TestPipeSecurityDescriptorIsOwnerOnly ties the local-only guarantee to the real
// code path on Windows: the SDDL startPipeTransport hands to winio.ListenPipe is
// built from the actual daemon-user SID (currentUserSID) and grants access to that
// SID alone — no Everyone / Authenticated-Users / anonymous ACE and no NULL DACL.
// That is the DACL half of "local-only"; the remote-rejection half is guaranteed by
// go-winio creating the pipe with FILE_PIPE_REJECT_REMOTE_CLIENTS (see
// startPipeTransport), and the baseline "the local owner CAN open it" is covered
// end-to-end by TestPipeTransportServesJSONRPC. A cross-host rejection test isn't
// feasible in CI, so this asserts exactly what is: no foreign principal is granted.
func TestPipeSecurityDescriptorIsOwnerOnly(t *testing.T) {
sid, err := currentUserSID()
if err != nil {
t.Fatalf("currentUserSID: %v", err)
}
if !strings.HasPrefix(sid, "S-1-") {
t.Fatalf("currentUserSID = %q, want a Windows SID (S-1-…)", sid)
}
sddl := ownerOnlySDDL(sid)
if !strings.Contains(sddl, sid) {
t.Fatalf("SDDL %q does not grant the daemon user's SID %q", sddl, sid)
}
if !strings.HasPrefix(sddl, "D:P") {
t.Errorf("DACL is not protected (missing D:P): %q", sddl)
}
for _, world := range []string{"S-1-1-0", "S-1-5-11", "S-1-5-7", "NO_ACCESS_CONTROL"} {
if strings.Contains(sddl, world) {
t.Errorf("SDDL grants a world/anonymous principal %q: %q", world, sddl)
}
}
}
// TestPipeTransportServesJSONRPC boots the production named-pipe transport
// (startPipeTransport + the real acceptLoop/serveConn dispatch), discovers the
// pipe name from rpc.pipe exactly as a client would, and drives the same JSON-RPC
// battery over it: an authenticated request round-trips identically to the socket,
// and an unauthenticated one is denied — the "no valid token → denied" half of the
// security guardrail, over real pipe I/O. (The owner-only-DACL half is asserted in
// TestOwnerOnlySDDL; impersonating a foreign SID in CI isn't feasible.)
func TestPipeTransportServesJSONRPC(t *testing.T) {
dir := t.TempDir()
sock := filepath.Join(dir, "rpc.sock")
pln, err := startPipeTransport(sock)
if err != nil {
t.Fatalf("startPipeTransport: %v", err)
}
s := &server{
token: testToken,
pipeLn: pln,
procs: newProcManager(),
conns: make(map[*conn]struct{}),
shutdown: make(chan struct{}),
}
go s.acceptLoop(s.pipeLn)
t.Cleanup(func() {
_ = pln.Close()
removePipeNameFile(sock)
s.procs.killAll()
})
// A client learns the opaque pipe name from rpc.pipe, written beside the socket
// before the pipe began accepting.
nameBytes, err := os.ReadFile(filepath.Join(dir, pipeNameFileName))
if err != nil {
t.Fatalf("read %s: %v", pipeNameFileName, err)
}
name := string(nameBytes)
if name != pln.Addr().String() {
t.Fatalf("rpc.pipe = %q, but listener addr = %q", name, pln.Addr())
}
timeout := 5 * time.Second
nc, err := winio.DialPipe(name, &timeout)
if err != nil {
t.Fatalf("dial pipe %s: %v", name, err)
}
cl := &testClient{t: t, nc: nc}
go cl.readLoop()
t.Cleanup(func() { _ = nc.Close() })
// Authenticated request round-trips through the exact same dispatch as AF_UNIX.
reply := string(cl.call(authed(`{"jsonrpc":"2.0","id":1,"method":"server.ping"}`)))
if !strings.Contains(reply, `"pong":true`) {
t.Fatalf("authed ping over pipe = %s, want pong:true", reply)
}
// A wrong token is rejected with the same Unauthorized error as on the socket.
denied := string(cl.call(`{"jsonrpc":"2.0","id":2,"method":"server.ping","auth":"wrong-token"}`))
if !strings.Contains(denied, "-32001") || !strings.Contains(denied, "Unauthorized") {
t.Fatalf("bad-token ping over pipe = %s, want -32001 Unauthorized", denied)
}
}