| 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 | } |