package main import ( "encoding/binary" "net" "strconv" "strings" ) // Pure parsers and classifiers for the observe plane — NO build tag, on // purpose. Every platform's enumeration output is decoded here, so the Linux // box that runs the tests exercises the macOS and Windows paths too. The // platform files (agents_.go) are left holding only the calls that // genuinely cannot run anywhere else: reading /proc, spawning ps/lsof/ss, and // the Win32 syscalls. Those are a handful of lines each; everything with a // decision or an offset in it lives in this file, under test. // binBase is filepath.Base for BOTH separator conventions, lowercased, with a // ".exe" suffix dropped. Deliberately not filepath.Base: that function follows // the separator of the machine it RUNS on, so a Windows argv would keep its // whole path when the same rule is exercised from a Linux test run. func binBase(s string) string { s = strings.ToLower(strings.TrimSpace(s)) if i := strings.LastIndexAny(s, `/\`); i >= 0 { s = s[i+1:] } return strings.TrimSuffix(s, ".exe") } // matchAgentSignature: argv[0] basename exact-match, OR (only for an interpreter) a distinctive // script-path substring. The interpreter gate kills the "merely mentions the marker" false positive. func matchAgentSignature(argv []string) (string, bool) { sig, auto, ok := matchSignature(argv) if !ok || auto { return "", false } return sig, true } // matchSignature is matchAgentSignature over BOTH tables: the agent tables // first (an agent that also drives a browser is an agent), then the // automation tables. `automation` reports which table answered. func matchSignature(argv []string) (sig string, automation bool, ok bool) { if len(argv) == 0 { return "", false, false } base := binBase(argv[0]) if sig, ok := agentBinaries[base]; ok { return sig, false, true } if sig, ok := automationBinaries[base]; ok { return sig, true, true } if interpreters[base] { full := strings.ToLower(strings.Join(argv, " ")) for _, s := range agentSubstrings { if strings.Contains(full, s.Sub) { return s.Sig, false, true } } for _, s := range automationSubstrings { if strings.Contains(full, s.Sub) { return s.Sig, true, true } } } return "", false, false } // ── Kind: agent, app, or client ───────────────────────────────────────────── // // The signature tables answer "which product is this binary". They cannot // answer "is this an agent", because the same binary name serves two products: // `claude.exe` is Claude Code (an agent) AND the Claude desktop app (a chat // window). The desktop app is Electron, and Electron — like every Chromium // host — runs its renderer, GPU, network and utility work in child processes // launched from the SAME executable with a `--type=` flag. One open app // therefore matched the agent table nine times, and eight of those were not // even the app: they were its helpers. // // Two rules, both measured on the box that produced the false alarm: // // helper — a `--type=` process WHOSE PARENT RUNS THE SAME PROGRAM (same // executable, or the same .app bundle on macOS). The flag alone // is not enough: an agent whose prompt text mentions "--type=json" // must stay an agent, and "add --type=x to your argv" must never be // a way to vanish from the report. A helper is dropped and its // parent remembered. // app — a matched process that owns helpers, or whose EXECUTABLE lives // inside a macOS .app bundle, is an APP: the user is running it. It // stays visible (what it talks to matters) but it is not an agent. // // Everything else the tables matched is an agent. A process caught by the // network signal alone is a client — unless the socket belongs to a helper, // in which case the APP that owns the helper is what talks to the model. const ( kindAgent = "agent" kindApp = "app" kindClient = "client" // automation: drives the machine with no model of its own — // a browser driver, an input injector, an RPA runtime. Inventory, never // an alert. Model traffic promotes it to an agent (mergeConnections). kindAutomation = "automation" ) // appSignatures renames a product signature when the process turns out to be // the interactive app rather than the agent, so the host never says // "claude-code" about a chat window. Unlisted products keep their name with an // "-app" suffix. var appSignatures = map[string]string{ "claude-code": "claude-desktop", } func appSignature(sig string) string { if s, ok := appSignatures[sig]; ok { return s } return sig + "-app" } // argv0 is the program a command line starts with: the first token, honouring // a quoted Windows path ("C:\Program Files\...\claude.exe" --type=renderer). func argv0(cmdline string) string { s := strings.TrimSpace(cmdline) if s == "" { return "" } if s[0] == '"' || s[0] == '\'' { if end := strings.IndexByte(s[1:], s[0]); end >= 0 { return s[1 : end+1] } return s[1:] } if i := strings.IndexByte(s, ' '); i >= 0 { return s[:i] } return s } // ── the kernel's executable path ──────────────────────────────────────────── // // Every platform answers procExe() through sanitizeExePath, so ONE rule decides // what counts as an executable path and it is tested from any OS. // // ABSOLUTE OR EMPTY. A relative path ("claude", "./bin/claude") names a file // only against a working directory nobody recorded, so it is not an identity — // two processes started from different directories would share it. macOS is // where this actually bites: the kernel saves the path as it was PASSED to // execve (xnu bsd/kern/kern_exec.c exec_save_path, which copyinstr's the // caller's string before namei resolves it), so a relative exec produces a // relative saved path. Linux (readlink /proc/pid/exe) and Windows // (QueryFullProcessImageName) always answer absolute. // // Dropping it yields "" — an observation that cannot be marked as yours — which // is the safe direction: a mark that names nothing checkable is worse than no // mark (sanction.go, agentSanction). func sanitizeExePath(s string) string { s = strings.TrimSpace(s) // Linux appends " (deleted)" once the file has been unlinked — measured on // this box: a running binary read "/tmp/x/sleepy" before `rm` and // "/tmp/x/sleepy (deleted)" after. STRIPPED, exactly as origin_linux.go:85 // already does, because the alternative silently un-marks a running agent // the moment its package updates in place, and a mark the user has to // re-apply after every update is a control they stop using (sanction.go, // STORAGE). It concedes nothing an attacker did not already have: writing a // file called "claude (deleted)" into that directory needs the same write // access as overwriting `claude` itself. s = strings.TrimSuffix(s, " (deleted)") if !isAbsExePath(s) { return "" } // REFUSED, NEVER TRUNCATED, past a bound no real path reaches. Truncation is // not available to this function whatever the length: two different // executables under one long directory would cut down to the SAME string and // therefore to the same identity, which is a collision in the one field that // is supposed to tell programs apart. So an absurd path becomes "" — // unmarkable, the safe direction — and this field cannot be used to inflate // the heartbeat it travels on (agents.go, agentObs.Exe). // // 4096 is Linux's PATH_MAX, the largest of the three kernels' own limits // (macOS is 1024; Windows' extended form goes further but nothing a person // installs lives out there). It admits every real path and bounds the field. if len(s) > 4096 { return "" } return s } // isAbsExePath answers for BOTH path conventions, deliberately not // filepath.IsAbs: that function follows the separator of the machine it RUNS // on, so a Windows path would read as relative under the Linux test run that // exercises this file — the same trap binBase() (agents_parse.go:18-21) is // written to avoid. func isAbsExePath(s string) bool { switch { case s == "": return false case s[0] == '/': return true // POSIX, and the \Device\... form never reaches here case strings.HasPrefix(s, `\\`): return true // UNC: \\server\share\thing.exe case len(s) >= 3 && s[1] == ':' && (s[2] == '\\' || s[2] == '/') && ((s[0] >= 'A' && s[0] <= 'Z') || (s[0] >= 'a' && s[0] <= 'z')): return true // C:\thing.exe } return false } // parseProcargs2ExePath pulls the kernel's saved executable path out of a macOS // KERN_PROCARGS2 blob. Lives here, with a fixture, because it is a byte layout // and the Linux box that runs the tests must exercise it (this file's header). // // The layout, from Apple's own two sources rather than from a blog: the kernel // writes argc as the first word and the executable path as the first string // after it (xnu bsd/kern/kern_sysctl.c sysctl_procargsx, which stores the path // prefixed with "executable_path=" and strips that prefix back off on the way // out), and Apple's ps walks the same blob by SKIPPING that string to reach // argv[0] — "/* Skip the saved exec_path. */" in adv_cmds ps/print.c, // getproclline. Only that prefix is decoded here; argv and the environment // follow and are none of this function's business. // // WHY THE PATH AND NOT `ps -o comm=`: because comm on macOS is argv[0]. The // same getproclline is what backs the `comm` keyword (ps/keyword.c maps it to // just_command), so it reports the string the launcher chose, which is the // exact value this whole change exists to stop trusting. origin_darwin.go's // header and origin.go's procFacts.Exe both used to say the opposite; // both now say what it is. func parseProcargs2ExePath(buf []byte) string { if len(buf) < 5 { return "" } // argc rides in the first word, native endian — and both machines macOS // runs on (amd64, arm64) are little-endian. if argc := int32(binary.LittleEndian.Uint32(buf[:4])); argc < 0 { return "" // not a blob this function understands; never guess past it } rest := buf[4:] end := 0 for end < len(rest) && rest[end] != 0 { end++ } if end == len(rest) { return "" // no terminator inside the buffer: a truncated read, not a path } return sanitizeExePath(string(rest[:end])) } // bundleRoot is the OUTERMOST macOS .app a path sits in, lowercased, or "". // "Google Chrome Helper.app" lives inside "Google Chrome.app/Contents/..."; the // outermost bundle is what makes a helper and its app the same program. func bundleRoot(path string) string { p := strings.ToLower(path) i := strings.Index(p, ".app/") if i < 0 { return "" } return p[:i+5] } // isAppBundle: the EXECUTABLE (argv[0], never an argument) lives inside a // macOS application bundle. func isAppBundle(cmdline string) bool { return bundleRoot(argv0(cmdline)) != "" } // hasTypeFlag: the raw Chromium marker. Necessary for a helper, never // sufficient — see isGUIHelper. func hasTypeFlag(cmdline string) bool { return strings.Contains(strings.ToLower(cmdline), " --type=") } // sameProgram: child and parent run the same executable, or live in the same // .app bundle. This is what makes a `--type=` process a helper rather than an // agent that happens to mention the flag. func sameProgram(childCmd, parentCmd string) bool { c, p := argv0(childCmd), argv0(parentCmd) if c == "" || p == "" { return false } if binBase(c) == binBase(p) { return true } if r := bundleRoot(c); r != "" && r == bundleRoot(p) { return true } return false } // isGUIHelper: a `--type=` process whose parent runs the same program. func isGUIHelper(cmdline, parentCmdline string) bool { return hasTypeFlag(cmdline) && sameProgram(cmdline, parentCmdline) } // maxHelperHops bounds the walk from a helper up to its app. Chromium nests // at most a few levels (browser → zygote → renderer); a chain longer than // this is a corrupt parent map, and the walk stops rather than loops. const maxHelperHops = 8 // classifyObservations applies the kind rules to a process snapshot IN PLACE: // helpers are removed, every survivor gets a kind and a parent, and an app's // signature is renamed. Returns helper pid -> the app pid that owns it, so the // caller can re-attribute the helpers' connections. // // A helper's parent must itself be in the snapshot: helpers run the same // executable as their app, so if the app matched the tables, so did they, and // if the parent is NOT a candidate the child is not its helper (an agent under // a shell). A helper whose parent cannot be read is kept as an agent — reported // rather than silently dropped, and never pinned to pid 0. func classifyObservations(byPid map[int]agentObs, parent func(int) int) map[int]int { cmdOf := func(a agentObs) string { if a.raw != "" { return a.raw } return a.Cmdline } helper := map[int]bool{} for pid, a := range byPid { pp := parent(pid) if pp <= 1 { continue } pa, ok := byPid[pp] if !ok { continue } if isGUIHelper(cmdOf(a), cmdOf(pa)) { helper[pid] = true } } helperRoot := map[int]int{} for pid := range helper { root := parent(pid) for hops := 0; helper[root] && hops < maxHelperHops; hops++ { root = parent(root) // the zygote pattern: a helper spawned by a helper } if root <= 1 || helper[root] { continue // unresolvable: leave it reported as what it matched } if _, ok := byPid[root]; !ok { continue } helperRoot[pid] = root delete(byPid, pid) } ownsHelpers := map[int]bool{} for _, root := range helperRoot { ownsHelpers[root] = true } for pid, a := range byPid { a.PPID = parent(pid) if ownsHelpers[pid] || isAppBundle(cmdOf(a)) { a.Kind = kindApp a.Signature = appSignature(a.Signature) } else if a.automation { a.Kind = kindAutomation } else { a.Kind = kindAgent } byPid[pid] = a } return helperRoot } // descendantsOf walks a pid -> parent map and returns every process below // root (children, grandchildren, ...), root excluded. Contain uses it so an // agent's shells and sub-processes go with it instead of being orphaned. A // cycle in a corrupt map terminates because each pid is visited once. func descendantsOf(parents map[int]int, root int) []int { children := map[int][]int{} for pid, pp := range parents { if pp > 0 && pid != pp { children[pp] = append(children[pp], pid) } } seen := map[int]bool{root: true} out := []int{} queue := []int{root} for len(queue) > 0 { cur := queue[0] queue = queue[1:] for _, c := range children[cur] { if seen[c] { continue } seen[c] = true out = append(out, c) queue = append(queue, c) } } return out } // parseProcStatPPID pulls the parent pid out of /proc//stat. The comm // field is parenthesised and may itself contain spaces or parens, so the // fields are counted from the LAST ')' — state is next, then ppid. func parseProcStatPPID(stat string) int { i := strings.LastIndex(stat, ")") if i < 0 { return 0 } f := strings.Fields(stat[i+1:]) if len(f) < 2 { return 0 } n, err := strconv.Atoi(f[1]) if err != nil || n < 0 { return 0 } return n } // parseProcStatStartTicks pulls the process start time out of /proc//stat. // Same technique as parseProcStatPPID and for the same reason — the comm field // is parenthesised and may itself contain spaces or parens, so the fields are // counted from the LAST ')'. // // starttime is field 22 of the whole line; the first field after ')' is field 3, // so it sits at index 19 of that slice. It counts clock ticks since boot, which // is what the delta's short-lived rule turns into an age (delta_linux.go). // // Here rather than in the Linux file, with no build tag, for the reason the top // of this file gives: one `go test` on any OS covers the byte layout, and a // parser only a Linux box can run is a parser only a Linux box checks. func parseProcStatStartTicks(stat string) (uint64, bool) { i := strings.LastIndex(stat, ")") if i < 0 { return 0, false } f := strings.Fields(stat[i+1:]) if len(f) < 20 { return 0, false } n, err := strconv.ParseUint(f[19], 10, 64) if err != nil { return 0, false } return n, true } // splitHostPortLoose parses the "address:port" forms the platform enumerators // hand back — `1.2.3.4:443`, `[fd00::1]:443`, `[::1]:11434` — into a bare IP // and a numeric port. Brackets are stripped so a v6 literal compares equal to // the same address from a DNS lookup. func splitHostPortLoose(s string) (string, int, bool) { s = strings.TrimSpace(s) if s == "" { return "", 0, false } host, port, err := net.SplitHostPort(s) if err != nil { i := strings.LastIndex(s, ":") if i < 0 { return "", 0, false } host, port = s[:i], s[i+1:] } host = strings.Trim(host, "[]") n, err := strconv.Atoi(port) if err != nil || n <= 0 || n > 65535 || host == "" { return "", 0, false } return host, n, true } // portOfAddr pulls the port out of a "host:port" env value (OLLAMA_ADDR, // PRIVATAE_LLM_ADDR). A bare host, a bare port, or junk yields 0 — never a guess. func portOfAddr(addr string) int { addr = strings.TrimSpace(addr) if addr == "" { return 0 } addr = strings.TrimPrefix(strings.TrimPrefix(addr, "http://"), "https://") if i := strings.IndexAny(addr, "/?"); i >= 0 { addr = addr[:i] } _, p, err := net.SplitHostPort(addr) if err != nil { return 0 } n, err := strconv.Atoi(p) if err != nil || n <= 0 || n > 65535 { return 0 } return n } // isLocalScope reports whether an address is on the user's own side of the // internet: loopback, RFC1918 / ULA, link-local, or CGNAT (100.64/10, which the // mesh itself uses and net.IP.IsPrivate does not cover). func isLocalScope(addr string) bool { ip := net.ParseIP(addr) if ip == nil { return false } if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() { return true } if v4 := ip.To4(); v4 != nil && v4[0] == 100 && v4[1] >= 64 && v4[1] <= 127 { return true } return false } // classifyConn names the inference endpoint a connection is talking to, and // says whether that alone is enough to call an unknown process an agent. // Returns ("", false) when the connection is not inference traffic. // // The label carries two INDEPENDENT facts, so neither one overstates: // // prefix address class — "local:" only when the peer really is on the // user's own side (loopback/LAN/mesh). A model served from a public // address is still self-hosted inference, but calling it "local" // reads as a lie, so it is named by its address instead. // suffix port confidence — a trailing "?" marks an ambiguous port that only // corroborates and never promotes an unknown process. func classifyConn(c connObs, cloud map[string]string, strong, weak map[int]string) (string, bool) { if host, ok := cloud[c.RemoteIP]; ok { return host, true } where := c.RemoteIP if isLocalScope(c.RemoteIP) { where = "local" } if name, ok := strong[c.RemotePort]; ok { return where + ":" + strconv.Itoa(c.RemotePort) + " (" + name + ")", true } if name, ok := weak[c.RemotePort]; ok { return where + ":" + strconv.Itoa(c.RemotePort) + " (" + name + "?)", false } return "", false } // procDetail is what the per-pid lookups answer for a process the signature // scan never matched. A STRUCT rather than the tuple this used to be // (user, cmdline, token), because the field added to it is the one field that // must never be confused with the command line: a positional fourth string is // one transposition away from writing argv[0] into Exe, which is the forgery // sanction.go exists to stop. The compiler now refuses that mistake. type procDetail struct { User string Cmdline string Token string // Exe is the KERNEL's executable path for the pid, or "" — see agentObs.Exe. Exe string } // mergeConnections folds the network signal into the process snapshot: an // endpoint annotates the agent (or app) that owns the connection, and a // STRONG endpoint on a process the signature scan missed becomes an // "llm-client" observation — the disguised case the network signal exists to // catch. A weak (ambiguous port) endpoint never creates an observation on its // own. // // GUI helpers: a socket held by a Chromium/Electron helper is the // APP's socket. A connection already re-attributed by the caller (ViaHelper) // annotates the app; an unknown `--type=` process found here is a helper only // if its parent runs the same program, and is then attributed up the chain to // the app. A process that merely mentions the flag is an ordinary client. func mergeConnections(byPid map[int]agentObs, conns []connObs, cloud map[string]string, strong, weak map[int]string, info func(int) procDetail, parent func(int) int) { annotate := func(pid int, label string, alone bool) bool { a, ok := byPid[pid] if !ok { return false } if !containsStr(a.LLMEndpoints, label) { a.LLMEndpoints = append(a.LLMEndpoints, label) } // An automation that talks to a model is an agent: the browser // driver was the hands, the model is the head. Only a strong // endpoint promotes — an ambiguous port annotates, as everywhere. if a.Kind == kindAutomation && alone { a.Kind = kindAgent } byPid[pid] = a return true } for _, c := range conns { label, alone := classifyConn(c, cloud, strong, weak) if label == "" { continue } if annotate(c.PID, label, alone) { continue } if !alone { continue } if c.ViaHelper { continue // the caller only rewrites to a root it kept, so this is unreachable; never invent one } d := info(c.PID) user, cmdline, token, exe := d.User, d.Cmdline, d.Token, d.Exe target, kind, sig := c.PID, kindClient, "llm-client" if hasTypeFlag(cmdline) { // Walk up while the parent runs the same program and is itself a // helper; the first same-program non-helper is the app. cur, curCmd := c.PID, cmdline resolved := false for hops := 0; hops < maxHelperHops; hops++ { root := parent(cur) if root <= 1 { break } rd := info(root) if !isGUIHelper(curCmd, rd.Cmdline) { break // not the same program: cur is what it is } if annotate(root, label, alone) { resolved = true break } if !hasTypeFlag(rd.Cmdline) { // The APP is now the observation, so its OWN exe travels // with it. Keeping the helper's would key a mark on a file // that is not the program the row names. target, user, cmdline, token, exe = root, rd.User, rd.Cmdline, rd.Token, rd.Exe kind, sig = kindApp, "gui-app" resolved = true break } cur, curCmd = root, rd.Cmdline } if resolved && target == c.PID { continue // annotated an existing observation up the chain } if !resolved && hasTypeFlag(cmdline) && target == c.PID { // A `--type=` process with no same-program parent (an agent // mentioning the flag, or an unreadable parent): a client. kind, sig = kindClient, "llm-client" } } byPid[target] = agentObs{ PID: target, PPID: parent(target), Kind: kind, User: user, Signature: sig, Cmdline: cmdline, Exe: exe, LLMEndpoints: []string{label}, DelegationToken: token, } } } // ── Linux: `ss -tnpH state established` ───────────────────────────────────── // pidFromSSLine extracts the pid from an ss process column: users:(("comm",pid=N,fd=M)). func pidFromSSLine(line string) int { i := strings.Index(line, "pid=") if i < 0 { return 0 } rest := line[i+4:] j := 0 for j < len(rest) && rest[j] >= '0' && rest[j] <= '9' { j++ } pid, _ := strconv.Atoi(rest[:j]) return pid } // parseSSOutput turns `ss -tnpH state established` into connections. Column 4 // is the peer address; the pid rides in the users:(...) column. func parseSSOutput(out string, self int) []connObs { conns := []connObs{} for _, line := range strings.Split(out, "\n") { f := strings.Fields(line) if len(f) < 4 { continue } ip, port, ok := splitHostPortLoose(f[3]) if !ok { continue } pid := pidFromSSLine(line) if pid > 0 && pid != self { conns = append(conns, connObs{PID: pid, RemoteIP: ip, RemotePort: port}) } } return conns } // ── macOS: `ps` and `lsof` ────────────────────────────────────────────────── // parsePSOutput turns `ps -axo pid=,ppid=,user=,args=` into per-pid parent, // user and argv. func parsePSOutput(out string) (map[int]string, map[int][]string, map[int]int) { users := map[int]string{} argvs := map[int][]string{} parents := map[int]int{} for _, line := range strings.Split(out, "\n") { f := strings.Fields(line) if len(f) < 4 { continue } pid, err := strconv.Atoi(f[0]) if err != nil || pid <= 0 { continue } ppid, err := strconv.Atoi(f[1]) if err != nil || ppid < 0 { continue } parents[pid] = ppid users[pid] = f[2] argvs[pid] = f[3:] } return users, argvs, parents } // parseLsofOutput reads lsof's machine-readable field output (-Fpn): a `p` // line opens a process block, and each following `n->` line is // one of its connections. func parseLsofOutput(out string, self int) []connObs { conns := []connObs{} pid := 0 for _, line := range strings.Split(out, "\n") { if len(line) < 2 { continue } switch line[0] { case 'p': n, err := strconv.Atoi(strings.TrimSpace(line[1:])) if err != nil { pid = 0 continue } pid = n case 'n': if pid <= 0 || pid == self { continue } i := strings.Index(line, "->") if i < 0 { continue } ip, port, ok := splitHostPortLoose(line[i+2:]) if !ok { continue } conns = append(conns, connObs{PID: pid, RemoteIP: ip, RemotePort: port}) } } return conns } // parseEnvTokenSpaced pulls one variable out of `ps -E` output, where the // environment is space-separated rather than NUL-separated. The value ends at // the next whitespace: a token with spaces in it could not survive this, and // the tokens the host mints (del_ + base64url) never contain any. func parseEnvTokenSpaced(s, key string) string { pre := key + "=" for _, field := range strings.Fields(s) { if strings.HasPrefix(field, pre) { return field[len(pre):] } } return "" } // ── Windows: the GetExtendedTcpTable buffers ──────────────────────────────── // // Parsed here rather than beside the syscall so the byte layout is under test // on the machine that runs CI. Layouts are the documented MIB structures; the // two row shapes differ enough to be worth separate constants: // // MIB_TCPROW_OWNER_PID (24B): state, localAddr, localPort, remoteAddr, remotePort, pid // MIB_TCP6ROW_OWNER_PID (56B): localAddr[16], localScope, localPort, // remoteAddr[16], remoteScope, remotePort, state, pid // // Both tables are { DWORD dwNumEntries; ROW table[] } with the rows at offset 4. // Ports are DWORDs whose LOW 16 bits hold the port in NETWORK byte order, so // the first two bytes of the little-endian DWORD are the port, big-endian. const ( winTCPRow4Size = 24 winTCPRow6Size = 56 winTCPStateEst = 5 // MIB_TCP_STATE_ESTAB ) func winPortAt(b []byte) int { if len(b) < 2 { return 0 } return int(binary.BigEndian.Uint16(b[:2])) } // parseTCPTable decodes an IPv4 or IPv6 owner-pid TCP table into the // established outbound connections owned by other processes. func parseTCPTable(buf []byte, v6 bool, self int) []connObs { conns := []connObs{} if len(buf) < 4 { return conns } n := int(binary.LittleEndian.Uint32(buf[:4])) rowSize := winTCPRow4Size if v6 { rowSize = winTCPRow6Size } for i := 0; i < n; i++ { off := 4 + i*rowSize if off+rowSize > len(buf) { break } row := buf[off : off+rowSize] var state, pid, port int var ip net.IP if v6 { port = winPortAt(row[44:48]) state = int(binary.LittleEndian.Uint32(row[48:52])) pid = int(binary.LittleEndian.Uint32(row[52:56])) ip = net.IP(append([]byte(nil), row[24:40]...)) } else { state = int(binary.LittleEndian.Uint32(row[0:4])) ip = net.IP(append([]byte(nil), row[12:16]...)) port = winPortAt(row[16:20]) pid = int(binary.LittleEndian.Uint32(row[20:24])) } if state != winTCPStateEst || pid <= 0 || pid == self || port == 0 { continue } conns = append(conns, connObs{PID: pid, RemoteIP: ip.String(), RemotePort: port}) } return conns } // parseWinEnvBlock finds one variable in a process environment block read out // of another process: UTF-16LE, NUL-separated entries, terminated by an empty // entry. Bounded by the caller's read; a truncated block simply stops early // rather than erroring, because a missing tag and an unreadable tag lead to // the same conservative outcome (the agent reads as unrequested). func parseWinEnvBlock(b []byte, key string) string { pre := strings.ToUpper(key) + "=" u16 := make([]uint16, 0, len(b)/2) for i := 0; i+1 < len(b); i += 2 { u16 = append(u16, uint16(b[i])|uint16(b[i+1])<<8) } start := 0 for i := 0; i <= len(u16); i++ { if i == len(u16) || u16[i] == 0 { if i == start { break // empty entry: end of block } entry := string(utf16Runes(u16[start:i])) if len(entry) >= len(pre) && strings.ToUpper(entry[:len(pre)]) == pre { return entry[len(pre):] } start = i + 1 } } return "" } // utf16Runes decodes a UTF-16 code-unit slice, including surrogate pairs. func utf16Runes(u []uint16) []rune { out := make([]rune, 0, len(u)) for i := 0; i < len(u); i++ { c := u[i] if c >= 0xD800 && c < 0xDC00 && i+1 < len(u) && u[i+1] >= 0xDC00 && u[i+1] < 0xE000 { out = append(out, ((rune(c)-0xD800)<<10|(rune(u[i+1])-0xDC00))+0x10000) i++ continue } out = append(out, rune(c)) } return out }