master
go 194 lines 5.67 KB
Raw
1 //go:build unix
2
3 // Package posix implements the L1 POSIX UDS SEQPACKET transport.
4 //
5 // Connection lifecycle, handshake with profile/limit negotiation,
6 // and send/receive with transparent chunking over AF_UNIX SEQPACKET sockets.
7 // Wire-compatible with the C and Rust implementations.
8 //
9 // Pure Go — no cgo. Works with CGO_ENABLED=0.
10 package posix
11
12 import (
13 "errors"
14 "fmt"
15 "path/filepath"
16 "syscall"
17 "unsafe"
18 )
19
20 // ---------------------------------------------------------------------------
21 // Constants
22 // ---------------------------------------------------------------------------
23
24 const (
25 defaultBacklog = 16
26 defaultBatchItems = 1
27 defaultPacketSizeFallback uint32 = 65536
28 helloPayloadSize = 44
29 helloAckPayloadSize = 48
30
31 // sun_path max — 108 on Linux, 104 on macOS/FreeBSD.
32 // We use a conservative limit.
33 maxSunPath = 104
34 )
35
36 // ---------------------------------------------------------------------------
37 // Errors
38 // ---------------------------------------------------------------------------
39
40 var (
41 ErrPathTooLong = errors.New("socket path exceeds sun_path limit")
42 ErrSocket = errors.New("socket syscall failed")
43 ErrConnect = errors.New("connect failed")
44 ErrAccept = errors.New("accept failed")
45 ErrSend = errors.New("send failed")
46 ErrRecv = errors.New("recv failed or peer disconnected")
47 ErrHandshake = errors.New("handshake protocol error")
48 ErrAuthFailed = errors.New("authentication token rejected")
49 ErrNoProfile = errors.New("no common transport profile")
50 ErrIncompatible = errors.New("protocol or layout version mismatch")
51 ErrProtocol = errors.New("wire protocol violation")
52 ErrAddrInUse = errors.New("address already in use by live server")
53 ErrChunk = errors.New("chunk header mismatch")
54 ErrLimitExceeded = errors.New("negotiated limit exceeded")
55 ErrBadParam = errors.New("invalid argument")
56 ErrDuplicateMsgID = errors.New("duplicate message_id")
57 ErrUnknownMsgID = errors.New("unknown response message_id")
58 )
59
60 // wrapErr creates a descriptive error wrapping a sentinel.
61 func wrapErr(sentinel error, detail string) error {
62 return fmt.Errorf("%w: %s", sentinel, detail)
63 }
64
65 // ---------------------------------------------------------------------------
66 // Internal helpers
67 // ---------------------------------------------------------------------------
68
69 // validateServiceName checks that name contains only [a-zA-Z0-9._-],
70 // is non-empty, and is not "." or "..".
71 func validateServiceName(name string) error {
72 if name == "" {
73 return wrapErr(ErrBadParam, "empty service name")
74 }
75 if name == "." || name == ".." {
76 return wrapErr(ErrBadParam, "service name cannot be '.' or '..'")
77 }
78 for i := 0; i < len(name); i++ {
79 c := name[i]
80 if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
81 (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-' {
82 continue
83 }
84 return wrapErr(ErrBadParam, fmt.Sprintf("service name contains invalid character: %q", c))
85 }
86 return nil
87 }
88
89 // buildSocketPath constructs {runDir}/{serviceName}.sock and validates length.
90 func buildSocketPath(runDir, serviceName string) (string, error) {
91 if err := validateServiceName(serviceName); err != nil {
92 return "", err
93 }
94 path := filepath.Join(runDir, serviceName+".sock")
95 // sun_path limit check. On Linux it's 108, macOS/FreeBSD 104.
96 // We use the smaller value for portability.
97 if len(path) >= maxSunPath {
98 return "", ErrPathTooLong
99 }
100 return path, nil
101 }
102
103 // detectPacketSize reads SO_SNDBUF from the socket.
104 func detectPacketSize(fd int) uint32 {
105 val, err := syscall.GetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_SNDBUF)
106 if err != nil || val <= 0 {
107 return defaultPacketSizeFallback
108 }
109 return uint32(val)
110 }
111
112 // highestBit returns the highest set bit in a bitmask (0 if empty).
113 func highestBit(mask uint32) uint32 {
114 if mask == 0 {
115 return 0
116 }
117 bit := uint32(1) << 31
118 for bit&mask == 0 {
119 bit >>= 1
120 }
121 return bit
122 }
123
124 func applyDefault(val, def uint32) uint32 {
125 if val == 0 {
126 return def
127 }
128 return val
129 }
130
131 func minU32(a, b uint32) uint32 {
132 if a < b {
133 return a
134 }
135 return b
136 }
137
138 // ---------------------------------------------------------------------------
139 // Low-level I/O
140 // ---------------------------------------------------------------------------
141
142 // rawSendIov sends header + payload as one SEQPACKET message using sendmsg.
143 func rawSendIov(fd int, hdr []byte, payload []byte) error {
144 var iov [2]syscall.Iovec
145 iov[0].Base = unsafe.SliceData(hdr) // #nosec G103 -- sendmsg iovec needs the backing byte-slice pointer.
146 iov[0].SetLen(len(hdr))
147
148 iovlen := uint64(1)
149 if len(payload) > 0 {
150 iov[1].Base = unsafe.SliceData(payload) // #nosec G103 -- sendmsg iovec needs the backing byte-slice pointer.
151 iov[1].SetLen(len(payload))
152 iovlen = 2
153 }
154
155 msg := syscall.Msghdr{
156 Iov: &iov[0],
157 Iovlen: iovlen,
158 }
159
160 n, _, errno := syscall.Syscall(
161 syscall.SYS_SENDMSG,
162 uintptr(fd),
163 uintptr(unsafe.Pointer(&msg)), // #nosec G103 -- raw sendmsg syscall requires a Msghdr pointer.
164 uintptr(syscall.MSG_NOSIGNAL),
165 )
166 if errno != 0 {
167 return wrapErr(ErrSend, errno.Error())
168 }
169
170 expected := len(hdr) + len(payload)
171 if int(n) != expected {
172 return wrapErr(ErrSend, fmt.Sprintf("short write: %d/%d", n, expected))
173 }
174 return nil
175 }
176
177 func ensureScratchBuf(buf *[]byte, needed int) []byte {
178 if len(*buf) < needed {
179 *buf = make([]byte, needed)
180 }
181 return (*buf)[:needed]
182 }
183
184 // rawRecv receives one SEQPACKET message. Returns bytes received.
185 func rawRecv(fd int, buf []byte) (int, error) {
186 n, _, _, _, err := syscall.Recvmsg(fd, buf, nil, 0)
187 if err != nil {
188 return 0, wrapErr(ErrRecv, err.Error())
189 }
190 if n == 0 {
191 return 0, wrapErr(ErrRecv, "peer disconnected")
192 }
193 return n, nil
194 }