master
go 328 lines 9.28 KB
Raw
1 //go:build windows
2
3 // Package windows implements the L1 Windows Named Pipe transport.
4 //
5 // Connection lifecycle, handshake with profile/limit negotiation,
6 // and send/receive with transparent chunking over Win32 Named Pipes
7 // in message mode. Wire-compatible with the C and Rust implementations.
8 //
9 // Pure Go — no cgo. Works with CGO_ENABLED=0.
10 package windows
11
12 import (
13 "errors"
14 "fmt"
15 "syscall"
16 "unicode/utf16"
17 "unsafe"
18 )
19
20 // ---------------------------------------------------------------------------
21 // Constants
22 // ---------------------------------------------------------------------------
23
24 const (
25 defaultBatchItems uint32 = 1
26 defaultPacketSize uint32 = 65536
27 defaultPipeBufSize uint32 = 65536
28 helloPayloadSize = 44
29 helloAckPayloadSize = 48
30 maxPipeNameChars = 256
31
32 // FNV-1a 64-bit constants
33 fnv1aOffsetBasis uint64 = 0xcbf29ce484222325
34 fnv1aPrime uint64 = 0x00000100000001B3
35 )
36
37 // Win32 constants
38 const (
39 _PIPE_ACCESS_DUPLEX = 0x00000003
40 _FILE_FLAG_FIRST_PIPE_INSTANCE = 0x00080000
41 _PIPE_TYPE_MESSAGE = 0x00000004
42 _PIPE_READMODE_MESSAGE = 0x00000002
43 _PIPE_WAIT = 0x00000000
44 _PIPE_UNLIMITED_INSTANCES = 255
45 _GENERIC_READ = 0x80000000
46 _GENERIC_WRITE = 0x40000000
47 _OPEN_EXISTING = 3
48
49 _ERROR_PIPE_CONNECTED = 535
50 _ERROR_BROKEN_PIPE = 109
51 _ERROR_NO_DATA = 232
52 _ERROR_PIPE_NOT_CONNECTED = 233
53 _ERROR_ACCESS_DENIED = 5
54 _ERROR_PIPE_BUSY = 231
55 )
56
57 // ---------------------------------------------------------------------------
58 // Errors
59 // ---------------------------------------------------------------------------
60
61 var (
62 ErrPipeName = errors.New("pipe name derivation failed")
63 ErrCreatePipe = errors.New("CreateNamedPipe failed")
64 ErrConnect = errors.New("connect failed")
65 ErrAccept = errors.New("accept failed")
66 ErrSend = errors.New("send failed")
67 ErrRecv = errors.New("recv failed or peer disconnected")
68 ErrHandshake = errors.New("handshake protocol error")
69 ErrAuthFailed = errors.New("authentication token rejected")
70 ErrNoProfile = errors.New("no common transport profile")
71 ErrIncompatible = errors.New("protocol or layout version mismatch")
72 ErrProtocol = errors.New("wire protocol violation")
73 ErrAddrInUse = errors.New("pipe name already in use by live server")
74 ErrChunk = errors.New("chunk header mismatch")
75 ErrLimitExceeded = errors.New("negotiated limit exceeded")
76 ErrBadParam = errors.New("invalid argument")
77 ErrDuplicateMsgID = errors.New("duplicate message_id")
78 ErrUnknownMsgID = errors.New("unknown response message_id")
79 ErrDisconnected = errors.New("peer disconnected")
80 )
81
82 func wrapErr(sentinel error, detail string) error {
83 return fmt.Errorf("%w: %s", sentinel, detail)
84 }
85
86 // ---------------------------------------------------------------------------
87 // Win32 syscall imports (pure Go, no cgo)
88 // ---------------------------------------------------------------------------
89
90 var (
91 modkernel32 = syscall.NewLazyDLL("kernel32.dll")
92
93 procCreateNamedPipeW = modkernel32.NewProc("CreateNamedPipeW")
94 procConnectNamedPipe = modkernel32.NewProc("ConnectNamedPipe")
95 procDisconnectNamedPipe = modkernel32.NewProc("DisconnectNamedPipe")
96 procFlushFileBuffers = modkernel32.NewProc("FlushFileBuffers")
97 procPeekNamedPipe = modkernel32.NewProc("PeekNamedPipe")
98 procSetNamedPipeHandleState = modkernel32.NewProc("SetNamedPipeHandleState")
99 procSwitchToThread = modkernel32.NewProc("SwitchToThread")
100 )
101
102 func createNamedPipe(name *uint16, openMode, pipeMode, maxInstances, outBufSize, inBufSize, defaultTimeout uint32) (syscall.Handle, error) {
103 r, _, err := procCreateNamedPipeW.Call(
104 uintptr(unsafe.Pointer(name)),
105 uintptr(openMode),
106 uintptr(pipeMode),
107 uintptr(maxInstances),
108 uintptr(outBufSize),
109 uintptr(inBufSize),
110 uintptr(defaultTimeout),
111 0, // NULL security attributes
112 )
113 handle := syscall.Handle(r)
114 if handle == syscall.InvalidHandle {
115 return handle, err
116 }
117 return handle, nil
118 }
119
120 func connectNamedPipe(handle syscall.Handle) error {
121 r, _, err := procConnectNamedPipe.Call(uintptr(handle), 0)
122 if r == 0 {
123 return err
124 }
125 return nil
126 }
127
128 func disconnectNamedPipe(handle syscall.Handle) {
129 procDisconnectNamedPipe.Call(uintptr(handle))
130 }
131
132 func flushFileBuffers(handle syscall.Handle) {
133 procFlushFileBuffers.Call(uintptr(handle))
134 }
135
136 func peekNamedPipeAvailable(handle syscall.Handle) (uint32, error) {
137 var available uint32
138 r, _, err := procPeekNamedPipe.Call(
139 uintptr(handle),
140 0,
141 0,
142 0,
143 uintptr(unsafe.Pointer(&available)),
144 0,
145 )
146 if r == 0 {
147 return 0, err
148 }
149 return available, nil
150 }
151
152 func setNamedPipeHandleState(handle syscall.Handle, mode *uint32) error {
153 r, _, err := procSetNamedPipeHandleState.Call(
154 uintptr(handle),
155 uintptr(unsafe.Pointer(mode)),
156 0, 0,
157 )
158 if r == 0 {
159 return err
160 }
161 return nil
162 }
163
164 // ---------------------------------------------------------------------------
165 // FNV-1a 64-bit hash
166 // ---------------------------------------------------------------------------
167
168 // FNV1a64 computes the FNV-1a 64-bit hash of data.
169 func FNV1a64(data []byte) uint64 {
170 hash := fnv1aOffsetBasis
171 for _, b := range data {
172 hash ^= uint64(b)
173 hash *= fnv1aPrime
174 }
175 return hash
176 }
177
178 // ---------------------------------------------------------------------------
179 // Service name validation
180 // ---------------------------------------------------------------------------
181
182 func validateServiceName(name string) error {
183 if name == "" {
184 return wrapErr(ErrBadParam, "empty service name")
185 }
186 if name == "." || name == ".." {
187 return wrapErr(ErrBadParam, "service name cannot be '.' or '..'")
188 }
189 for i := 0; i < len(name); i++ {
190 c := name[i]
191 if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
192 (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-' {
193 continue
194 }
195 return wrapErr(ErrBadParam, fmt.Sprintf("service name contains invalid character: %q", c))
196 }
197 return nil
198 }
199
200 // ---------------------------------------------------------------------------
201 // Pipe name derivation
202 // ---------------------------------------------------------------------------
203
204 // BuildPipeName constructs the Named Pipe path from run_dir and service_name.
205 // Returns the pipe name as a NUL-terminated UTF-16 slice.
206 func BuildPipeName(runDir, serviceName string) ([]uint16, error) {
207 if err := validateServiceName(serviceName); err != nil {
208 return nil, err
209 }
210
211 hash := FNV1a64([]byte(runDir))
212 narrow := fmt.Sprintf(`\\.\pipe\netipc-%016x-%s`, hash, serviceName)
213
214 if len(narrow) >= maxPipeNameChars {
215 return nil, wrapErr(ErrPipeName, "pipe name too long")
216 }
217
218 return utf16.Encode(append([]rune(narrow), 0)), nil
219 }
220
221 // ---------------------------------------------------------------------------
222 // Internal helpers
223 // ---------------------------------------------------------------------------
224
225 func applyDefault(val, def uint32) uint32 {
226 if val == 0 {
227 return def
228 }
229 return val
230 }
231
232 func minU32(a, b uint32) uint32 {
233 if a < b {
234 return a
235 }
236 return b
237 }
238
239 func maxU32(a, b uint32) uint32 {
240 if a > b {
241 return a
242 }
243 return b
244 }
245
246 func pipeBufferSize(packetSize uint32) uint32 {
247 // The protocol packet size controls logical framing and chunk size. The
248 // underlying pipe quota must stay large enough for full-duplex pipelining
249 // even when tests force a tiny protocol packet size.
250 return maxU32(applyDefault(packetSize, defaultPipeBufSize), defaultPipeBufSize)
251 }
252
253 func highestBit(mask uint32) uint32 {
254 if mask == 0 {
255 return 0
256 }
257 bit := uint32(1) << 31
258 for bit&mask == 0 {
259 bit >>= 1
260 }
261 return bit
262 }
263
264 func isDisconnectError(err error) bool {
265 errno, ok := err.(syscall.Errno)
266 if !ok {
267 return false
268 }
269 return errno == _ERROR_BROKEN_PIPE ||
270 errno == _ERROR_NO_DATA ||
271 errno == _ERROR_PIPE_NOT_CONNECTED
272 }
273
274 // ---------------------------------------------------------------------------
275 // Low-level I/O
276 // ---------------------------------------------------------------------------
277
278 func rawWrite(handle syscall.Handle, data []byte) error {
279 var written uint32
280 err := syscall.WriteFile(handle, data, &written, nil)
281 if err != nil {
282 if isDisconnectError(err) {
283 return ErrDisconnected
284 }
285 return wrapErr(ErrSend, err.Error())
286 }
287 if written != uint32(len(data)) {
288 return wrapErr(ErrSend, fmt.Sprintf("short write: %d/%d", written, len(data)))
289 }
290 return nil
291 }
292
293 func rawSendMsg(handle syscall.Handle, msg []byte) error {
294 return rawWrite(handle, msg)
295 }
296
297 func rawRecv(handle syscall.Handle, buf []byte) (int, error) {
298 var read uint32
299 err := syscall.ReadFile(handle, buf, &read, nil)
300 if err != nil {
301 // ERROR_MORE_DATA (234): message mode pipe message is larger
302 // than the buffer. The data read so far is valid; the
303 // remaining data can be read with another ReadFile call.
304 // For our protocol this should not happen if the buffer is
305 // sized correctly, but treat it as a successful partial read
306 // rather than a fatal error.
307 if err == syscall.Errno(234) {
308 if read > 0 {
309 return int(read), nil
310 }
311 }
312 if isDisconnectError(err) {
313 return 0, ErrDisconnected
314 }
315 return 0, wrapErr(ErrRecv, err.Error())
316 }
317 if read == 0 {
318 return 0, ErrDisconnected
319 }
320 return int(read), nil
321 }
322
323 func ensurePipeScratchBuf(buf *[]byte, needed int) []byte {
324 if len(*buf) < needed {
325 *buf = make([]byte, needed)
326 }
327 return (*buf)[:needed]
328 }