@passioncode-ai/fabric-agent-adapter 0.81.0 → 0.83.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +200 -3
- package/README.md +11 -6
- package/SECURITY.md +8 -5
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +7 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +9 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +4 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +18 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +25 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +19 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +31 -15
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +274 -17
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +24 -151
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check_cli.py +164 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_tree.py +3 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +17 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +24 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +14 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/README.md +76 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/config.example.json +28 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/data_agent.py +366 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +32 -17
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +4 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support-agent.mjs +115 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +63 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/a2a.md +98 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +23 -10
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +9 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +14 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +28 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/ops-digest.md +75 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +7 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +30 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +10 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +70 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +19 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_a2a.py +158 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +7 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +22 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a-server.mjs +274 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a.mjs +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +14 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +54 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +45 -31
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +16 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs +29 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +5 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +18 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +6 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +27 -10
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +3 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +21 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +20 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +88 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-mandate.mjs +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +24 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ops-digest.mjs +455 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +17 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +18 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +133 -30
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +44 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +3 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-socket-proof.mjs +180 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +5 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet-identity.mjs +468 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a.py +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a_server.py +404 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +16 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +97 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +35 -48
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +15 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +35 -159
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +18 -13
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +23 -14
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +43 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +21 -18
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +39 -17
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +55 -96
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +98 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +92 -10
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +43 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ops_digest.py +647 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +16 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +20 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +180 -162
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +274 -17
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +3 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_socket_proof.py +390 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +3 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet_identity.py +492 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +465 -55
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample-remote-service.mjs +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +16 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/tailnet-groups.schema.json +69 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +5 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +16 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-that-answer-people.md +14 -9
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +9 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +4 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +18 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +2 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/admit.py +162 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +19 -1
- package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +2 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +30 -17
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +53 -35
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +10 -5
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +2 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +34 -21
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +96 -15
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +147 -45
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py
CHANGED
|
@@ -1,110 +1,79 @@
|
|
|
1
1
|
"""peer_owner: which OS account owns each end of a loopback TCP connection.
|
|
2
2
|
|
|
3
|
-
peer_owner 1.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
and a
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
`TABLE_RESTRICTED`, and a caller may vouch for a listener with a pid it started (`pid_listens`,
|
|
25
|
-
`parse_lsof_listeners`, `LSOF`, `CheckedHTTPConnection(vouch_pid=...)`, `own_connection(vouch_pid=...)`); since kit.3
|
|
26
|
-
the child's port is waited on with `selectors` and read under one deadline (FAA-273); since kit.4 a number is ASCII
|
|
27
|
-
digits only and a netstat with only its header reads as `TABLE_RESTRICTED` (FAA-273 items 3 and 4).
|
|
28
|
-
A limit /proc sets: it carries a socket's creator and no pid, so a listening socket that root
|
|
29
|
-
created and handed to another account (systemd socket activation with User=) reads as root's. Standard library only, no import of another kit module.
|
|
30
|
-
The kit's tests are `test/test_fabric_peer_owner.py`, over `test/fixtures/peer-owner/`;
|
|
31
|
-
its Node twin is `fabric-peer-owner.mjs` (netstat, /proc and PowerShell, no ctypes).
|
|
32
|
-
|
|
33
|
-
WHY. Loopback is one per machine, not one per OS account. On a host with several accounts,
|
|
34
|
-
every account's processes reach every service any account listens with on 127.0.0.1, and a
|
|
35
|
-
loopback socket proves nothing about who is on the other end. This module answers that
|
|
36
|
-
question from the host's socket table, read without privileges:
|
|
3
|
+
peer_owner 1.3.1+kit.7: Observatory's peer_owner (passioncode-ai/project-observatory-dashboard,
|
|
4
|
+
`observatory/engine/peer_owner.py`, vendored at f86f0622, read against 2180235c for 1.2.0 and a69c6fee for 1.3.1),
|
|
5
|
+
the kit's `fabric_peer_owner` (layer L0; DEC-0154 HA-2, FAA-226, FAA-258). 1.3.1's proof from this account's own
|
|
6
|
+
sockets (DEC-0155 RT-2) is the kit's `fabric_socket_proof`, asked when it sits beside this file (FAA-292); without
|
|
7
|
+
it this file stands alone, as before. The kit's differences: NamedTuples (a service may load a kit file by path);
|
|
8
|
+
the Python 3.9 floor; `classify` / `classify_sid` ask for this process's uid or SID first (a client running as root
|
|
9
|
+
owns root's listener); on Linux `server_end` reads an accepted connection's own row and, for one not accept()ed yet
|
|
10
|
+
(uid 0, inode 0), the listener on its local address (CT-254), several rows of one owner are that owner, a row
|
|
11
|
+
printed twice counts once, and a `/proc` question that finds no row or several reads once more. On macOS (FAA-267,
|
|
12
|
+
FAA-273): the sysctl calibration also requires a child's listener, read under one deadline with `selectors`; a
|
|
13
|
+
number is ASCII digits only; a caller may vouch for a listener with a pid it started (`pid_listens`,
|
|
14
|
+
`CheckedHTTPConnection(vouch_pid=...)`, RT-3, the fallback after RT-2). DEC-0155 (FAA-312): a netstat that fails or
|
|
15
|
+
is empty, a table with no row of another uid (sysctl) or process (netstat) and a missing connection row read as
|
|
16
|
+
`table-restricted` (RT-1); a restricted table's listener is this account's only on exactly 127.0.0.1 (RT-4); no
|
|
17
|
+
proof is taken from a set-uid reader. `main` is the CLI, `listener <port>` (FAA-315). /proc carries a socket's
|
|
18
|
+
creator, so a listener root created and handed to another account (socket activation) reads as root's. Tests:
|
|
19
|
+
`test/test_fabric_peer_owner.py`; the Node twin is `fabric-peer-owner.mjs`.
|
|
20
|
+
|
|
21
|
+
WHY. Loopback is one per machine, not one per OS account. On a host with several accounts, every account's processes
|
|
22
|
+
reach every service any account listens with on 127.0.0.1, and a loopback socket proves nothing about who is on the
|
|
23
|
+
other end. This module answers that question from the host's socket table, read without privileges:
|
|
37
24
|
|
|
38
25
|
listener(port) -> Owner who listens on 127.0.0.1:<port> (also 0.0.0.0, ::1, ::)
|
|
39
26
|
server_end(sock) -> Owner for a client's connected socket: who accepted it
|
|
40
27
|
peer(sock) -> Owner for a server's accepted socket: who connected
|
|
41
28
|
peer_at(server, client) the same as `peer`, from two (host, port) addresses
|
|
42
|
-
own_connection(port, ...) a CheckedHTTPConnection, connected: every connect
|
|
43
|
-
|
|
44
|
-
that the server end is this account's, or raises ListenerRefused
|
|
45
|
-
before a byte of the request is sent
|
|
29
|
+
own_connection(port, ...) a CheckedHTTPConnection, connected: every connect (the first, and any reconnect
|
|
30
|
+
http.client makes) checks the server end is this account's before a byte is sent
|
|
46
31
|
|
|
47
32
|
`Owner.kind` is one of:
|
|
48
33
|
|
|
49
34
|
this-account the socket belongs to the account running this process
|
|
50
35
|
another-account another OS account's
|
|
51
|
-
system uid 0, pid 1 (launchd
|
|
52
|
-
Windows, the system's own accounts and processes
|
|
36
|
+
system uid 0, pid 1 (launchd's socket-activated jobs) or, on Windows, the system's accounts and processes
|
|
53
37
|
unknown the owner could not be told; `reason` says why
|
|
54
38
|
none `listener` only: nothing listens on that port
|
|
55
39
|
|
|
56
|
-
A caller that must not leak (a client about to send a bearer, a server on a multi-account
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
answer
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
`
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
- macOS, fallback (`netstat`): `/usr/sbin/netstat -anv -p tcp`, its `process:pid` column, then
|
|
83
|
-
`ps -o uid= -p PID` for the owner of the one row a question needs. Measured: about 20 ms over
|
|
84
|
-
846 sockets, and it names the pids of root's listeners, which `lsof` run by an ordinary account
|
|
85
|
-
cannot see. Another local service implemented the same check this way; it is kept as the
|
|
86
|
-
documented fallback for a macOS whose record layout the calibration refuses.
|
|
87
|
-
- Linux (`proc`): `/proc/net/tcp` and `/proc/net/tcp6`, whose `uid` column every account may
|
|
88
|
-
read (also under `hidepid=2`, which hides `/proc/<pid>` and not this file). No pid: that is
|
|
89
|
-
only readable for this account's own processes, and the decision needs the uid alone.
|
|
90
|
-
- Windows (`iphlpapi`): `GetExtendedTcpTable(TCP_TABLE_OWNER_PID_ALL)` gives each row's owning
|
|
91
|
-
pid; `OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)`, `OpenProcessToken(TOKEN_QUERY)` and
|
|
92
|
-
`GetTokenInformation(TokenUser)` give its account SID, compared with this process's. A process
|
|
93
|
-
that cannot be opened (another account's, usually) is `unknown`, never `another-account`:
|
|
94
|
-
the caller refuses either way, and the reason says which it was. `Owner.uid` is None there.
|
|
40
|
+
A caller that must not leak (a client about to send a bearer, a server on a multi-account host about to read a request)
|
|
41
|
+
treats everything but `this-account` as a refusal, `unknown` included: fail closed.
|
|
42
|
+
|
|
43
|
+
HOW, ON EACH OS. Every read is bounded (MAX_TABLE_BYTES of table, TIMEOUT_SECONDS) and the answer is never cached across
|
|
44
|
+
connections; only `listener` is cached, for LISTENER_TTL. A TIME_WAIT row is a connection already closed and is never an
|
|
45
|
+
answer; two live rows with the same addresses are `unknown` (`several-rows`), never a guess.
|
|
46
|
+
|
|
47
|
+
- macOS, primary (`sysctl`): `sysctl net.inet.tcp.pcblist_n` through ctypes, the record list `netstat` itself reads.
|
|
48
|
+
Each connection is an `xinpcb_n` (addresses and ports) followed by an `xsocket_n` that carries `so_uid` and
|
|
49
|
+
`so_last_pid` (XNU `bsd/netinet/in_pcb.h` and `bsd/sys/socketvar.h`, both under `#pragma pack(4)`), paired by the pcb
|
|
50
|
+
handle (`xi_inpp` equals `so_pcb`); the `xtcpcb_n` that follows gives the TCP state. Measured on macOS 26: about 4 ms
|
|
51
|
+
for 770 sockets. The layout is not an API, so the first read in a process calibrates it on a connected pair of its
|
|
52
|
+
own (the listener and both ends must read back with their exact addresses, this uid and this pid); a layout that
|
|
53
|
+
fails is not used again in that process.
|
|
54
|
+
- macOS 27.2 shows an ad-hoc-signed interpreter (a Homebrew Python) only its own sockets: the calibration also requires
|
|
55
|
+
a child's listener, and a restricted table (DEC-0155 RT-1) is `unknown` with a reason beginning `table-restricted`,
|
|
56
|
+
never another account's, unless `fabric_socket_proof` finds the socket in this account's own processes (RT-2) or a
|
|
57
|
+
caller that started the server vouches with its pid, which `lsof -p PID` asks the process itself about (RT-3).
|
|
58
|
+
- macOS, fallback (`netstat`): `/usr/sbin/netstat -anv -p tcp`, its `process:pid` column, then `ps -o uid= -p PID` for
|
|
59
|
+
the one row a question needs; about 20 ms over 846 sockets, and it names the pids of root's listeners, which `lsof`
|
|
60
|
+
run by an ordinary account cannot see. Kept for a macOS whose record layout the calibration refuses.
|
|
61
|
+
- Linux (`proc`): `/proc/net/tcp` and `/proc/net/tcp6`, whose `uid` column every account may read (also under
|
|
62
|
+
`hidepid=2`). No pid: that is only readable for this account's own processes, and the decision needs the uid alone.
|
|
63
|
+
- Windows (`iphlpapi`): `GetExtendedTcpTable(TCP_TABLE_OWNER_PID_ALL)` gives each row's owning pid; `OpenProcess`,
|
|
64
|
+
`OpenProcessToken` and `GetTokenInformation(TokenUser)` its account SID, compared with this process's. A process that
|
|
65
|
+
cannot be opened (another account's, usually) is `unknown`, never `another-account`. `Owner.uid` is None there.
|
|
95
66
|
- Anything else: `unknown`, reason `unsupported-platform`.
|
|
96
67
|
|
|
97
|
-
`LOCAL_PEERCRED`, `getpeereid` and
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
WHAT IT NEVER DOES. It never reads a byte from the socket it is asked about, never resolves a
|
|
101
|
-
name, never sends anything, and never needs a privilege. It reports the process names
|
|
102
|
-
`netstat` prints only as far as the pid behind them.
|
|
68
|
+
`LOCAL_PEERCRED`, `getpeereid` and `SO_PEERCRED` answer this for Unix-domain sockets only, hence a socket table. It
|
|
69
|
+
never reads a byte from the socket it is asked about, never resolves a name, never sends anything, never needs a
|
|
70
|
+
privilege, and reports the process names `netstat` prints only as far as the pid behind them.
|
|
103
71
|
"""
|
|
104
72
|
from __future__ import annotations
|
|
105
73
|
|
|
106
74
|
import http.client
|
|
107
75
|
import ipaddress
|
|
76
|
+
import json
|
|
108
77
|
import os
|
|
109
78
|
import socket
|
|
110
79
|
import struct
|
|
@@ -115,7 +84,7 @@ import time
|
|
|
115
84
|
from typing import NamedTuple, Optional, Tuple
|
|
116
85
|
|
|
117
86
|
# This file's own version, for projects that copy it; not the engine's (that is configuration.VERSION).
|
|
118
|
-
PEER_OWNER_VERSION = "1.
|
|
87
|
+
PEER_OWNER_VERSION = "1.3.1+kit.7" # Observatory's 1.3.1 line with the kit's differences above
|
|
119
88
|
|
|
120
89
|
THIS_ACCOUNT = "this-account"
|
|
121
90
|
ANOTHER_ACCOUNT = "another-account"
|
|
@@ -124,10 +93,8 @@ UNKNOWN = "unknown"
|
|
|
124
93
|
NONE = "none"
|
|
125
94
|
KINDS = (THIS_ACCOUNT, ANOTHER_ACCOUNT, SYSTEM, UNKNOWN, NONE)
|
|
126
95
|
|
|
127
|
-
#: The most table bytes one read accepts
|
|
128
|
-
#:
|
|
129
|
-
#: developer machine passes that. 4 MiB holds about 6,800 at the 616 bytes a socket measured on macOS 26
|
|
130
|
-
#: (0.74.0 product reading 11, FAA-260).
|
|
96
|
+
#: The most table bytes one read accepts: 1 MB held about 1,200 sockets at `pcblist_n`'s 860 bytes each, which a busy
|
|
97
|
+
#: developer machine passes; 4 MiB holds about 6,800 at 616 bytes measured on macOS 26 (FAA-260).
|
|
131
98
|
MAX_TABLE_BYTES = 4 * 1024 * 1024
|
|
132
99
|
#: Every subprocess this module starts is stopped after this many seconds.
|
|
133
100
|
TIMEOUT_SECONDS = 2.0
|
|
@@ -141,6 +108,8 @@ LSOF = "/usr/sbin/lsof"
|
|
|
141
108
|
#: The reason a socket table hides other processes' sockets from this one (FAA-267: macOS 27.2 shows an ad-hoc-signed
|
|
142
109
|
#: interpreter, such as a Homebrew Python, only its own sockets through sysctl and nothing through netstat).
|
|
143
110
|
TABLE_RESTRICTED = "table-restricted"
|
|
111
|
+
#: A macOS table that lists sockets but not the reader's own connection (DEC-0155 RT-1, FAA-312).
|
|
112
|
+
NO_ROW_RESTRICTED = TABLE_RESTRICTED + ": no row for this connection"
|
|
144
113
|
|
|
145
114
|
#: Client codes of `own_connection` (ListenerRefused.code).
|
|
146
115
|
SERVER_NOT_OWN = "server-not-own"
|
|
@@ -167,8 +136,7 @@ class Owner(_OwnerFields):
|
|
|
167
136
|
|
|
168
137
|
|
|
169
138
|
class Row(_RowFields):
|
|
170
|
-
"""One TCP socket of the host's table
|
|
171
|
-
(netstat, Windows): it is resolved from `pid` only for the row a question selects."""
|
|
139
|
+
"""One TCP socket of the host's table; `uid` is None where it carries none (netstat, Windows): read from `pid`."""
|
|
172
140
|
__slots__ = ()
|
|
173
141
|
|
|
174
142
|
|
|
@@ -188,9 +156,8 @@ class ListenerRefused(Exception):
|
|
|
188
156
|
# -- addresses --------------------------------------------------------------------------------------------------------
|
|
189
157
|
|
|
190
158
|
def norm_host(host: str) -> str:
|
|
191
|
-
"""One spelling per address: an IPv4-mapped IPv6 address is its IPv4 form, `*` is the
|
|
192
|
-
|
|
193
|
-
only itself)."""
|
|
159
|
+
"""One spelling per address: an IPv4-mapped IPv6 address is its IPv4 form, `*` is the wildcard, `localhost` is
|
|
160
|
+
127.0.0.1. Anything unparseable is returned unchanged (and matches only itself)."""
|
|
194
161
|
if host in ("*", ""):
|
|
195
162
|
return "*"
|
|
196
163
|
if host == "localhost":
|
|
@@ -215,12 +182,16 @@ def _address(addr) -> tuple[str, int]:
|
|
|
215
182
|
# -- classification ---------------------------------------------------------------------------------------------------
|
|
216
183
|
|
|
217
184
|
def _posix_uid() -> Optional[int]:
|
|
218
|
-
"""This process's uid, or None where the OS has none (Windows compares SIDs
|
|
219
|
-
|
|
220
|
-
is read here, once, on the POSIX paths only (an engine boundary test names this function)."""
|
|
185
|
+
"""This process's uid, or None where the OS has none (Windows compares SIDs). The one place this module reads it:
|
|
186
|
+
vendored without its project's OS layer, it reads it here, on POSIX only (an engine boundary test names it)."""
|
|
221
187
|
return os.getuid() if hasattr(os, "getuid") else None
|
|
222
188
|
|
|
223
189
|
|
|
190
|
+
def _set_uid() -> bool:
|
|
191
|
+
"""Real and effective uid differ: such a reader uses none of DEC-0155's proofs (RT-2 to RT-5), the vouch included."""
|
|
192
|
+
return hasattr(os, "geteuid") and os.geteuid() != _posix_uid()
|
|
193
|
+
|
|
194
|
+
|
|
224
195
|
def classify(uid: Optional[int], pid: Optional[int], *, method: str, own_uid: Optional[int] = None) -> Owner:
|
|
225
196
|
"""`this-account` for this process's uid, `system` for uid 0 or pid 1, else `another-account`. A client that runs
|
|
226
197
|
as root itself reads root's listener as its own account's (the kit's order; the vendored file put uid 0 first)."""
|
|
@@ -310,12 +281,11 @@ def parse_ps_uid(text: str) -> Optional[int]:
|
|
|
310
281
|
|
|
311
282
|
|
|
312
283
|
def parse_proc_net_tcp(text: str, *, family: int, byteorder: Optional[str] = None) -> list[Row]:
|
|
313
|
-
"""`/proc/net/tcp` (family AF_INET) or `/proc/net/tcp6` (AF_INET6). The kernel prints each
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
unknown (the kit's addition, 0.74.0 unit readings 3 and 4)."""
|
|
284
|
+
"""`/proc/net/tcp` (family AF_INET) or `/proc/net/tcp6` (AF_INET6). The kernel prints each 32-bit word of an address
|
|
285
|
+
in host byte order and each port in network order, in hex. A connected row with inode 0 has no socket yet (a
|
|
286
|
+
connection the server has not accept()ed, or one already closed): the kernel prints uid 0 for it, so its uid is not
|
|
287
|
+
read (None); `_owner_of` reads a server end not yet accepted as its listener's and a closed client as unknown (the
|
|
288
|
+
kit's addition, 0.74.0 unit readings 3 and 4)."""
|
|
319
289
|
order = byteorder or sys.byteorder
|
|
320
290
|
rows: list[Row] = []
|
|
321
291
|
for line in text.splitlines()[1:]:
|
|
@@ -540,10 +510,9 @@ def _child_line(child: "subprocess.Popen[bytes]", deadline: float) -> Optional[b
|
|
|
540
510
|
|
|
541
511
|
|
|
542
512
|
def _child_listener_seen(rows_of) -> Optional[bool]:
|
|
543
|
-
"""Whether the table shows a listener of a child process of this one: False when it
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
pipe that cannot be read). The calibration trusts only True."""
|
|
513
|
+
"""Whether the table shows a listener of a child process of this one: False when not (it shows this process alone,
|
|
514
|
+
FAA-267), None when no child could be started (a frozen or embedded host) or it gave no port line within
|
|
515
|
+
TIMEOUT_SECONDS (silence, a partial line, an early exit, an unreadable pipe). The calibration trusts only True."""
|
|
547
516
|
if not sys.executable or getattr(sys, "frozen", False):
|
|
548
517
|
return None
|
|
549
518
|
try:
|
|
@@ -624,7 +593,12 @@ def read_table() -> tuple[list[Row], str]:
|
|
|
624
593
|
except (TableUnreadable, struct.error, ValueError):
|
|
625
594
|
how = "netstat" # this read only; calibration stands
|
|
626
595
|
if how == "netstat":
|
|
627
|
-
|
|
596
|
+
try:
|
|
597
|
+
text = _run([NETSTAT, "-anv", "-p", "tcp"])
|
|
598
|
+
except TableUnreadable as exc:
|
|
599
|
+
# A netstat that fails, exits non-zero or times out is a restricted table (DEC-0155 RT-1, FAA-312).
|
|
600
|
+
raise TableUnreadable(f"{TABLE_RESTRICTED}: {exc}") from None
|
|
601
|
+
return parse_netstat(text), how
|
|
628
602
|
if how == "proc":
|
|
629
603
|
rows: list[Row] = []
|
|
630
604
|
for name, family in (("/proc/net/tcp", socket.AF_INET), ("/proc/net/tcp6", socket.AF_INET6)):
|
|
@@ -779,6 +753,18 @@ def _windows_owner(pid: Optional[int], how: str) -> Owner: # pragma: no cover -
|
|
|
779
753
|
|
|
780
754
|
# -- the questions ----------------------------------------------------------------------------------------------------
|
|
781
755
|
|
|
756
|
+
def restricted(rows: list[Row], how: str, *, own_uid: Optional[int] = None, own_pid: Optional[int] = None) -> bool:
|
|
757
|
+
"""DEC-0155 RT-1 (FAA-312): a macOS table holding no row of another uid (sysctl, which carries uids) or of another
|
|
758
|
+
process (netstat, which carries pids) is restricted, and a missing row proves nothing. Never on Linux or Windows."""
|
|
759
|
+
if how == "sysctl":
|
|
760
|
+
own = _posix_uid() if own_uid is None else own_uid
|
|
761
|
+
return not any(row.uid is not None and row.uid != own for row in rows)
|
|
762
|
+
if how == "netstat":
|
|
763
|
+
own = os.getpid() if own_pid is None else own_pid
|
|
764
|
+
return not any(row.pid is not None and row.pid != own for row in rows)
|
|
765
|
+
return False
|
|
766
|
+
|
|
767
|
+
|
|
782
768
|
def find(rows: list[Row], local: tuple[str, int], remote: tuple[str, int]) -> list[Row]:
|
|
783
769
|
"""The live connected rows with exactly this local and remote address: one, or the caller
|
|
784
770
|
cannot tell. A TIME_WAIT row is a connection already closed and is left out. A row printed twice is one
|
|
@@ -799,19 +785,14 @@ def _owner_of(local: tuple[str, int], remote: tuple[str, int], server: bool = Fa
|
|
|
799
785
|
if how != "proc" or len(found) == 1:
|
|
800
786
|
break
|
|
801
787
|
# A /proc read under churn can skip a row or print one twice; one more read before answering unknown.
|
|
802
|
-
if not found:
|
|
803
|
-
return Owner(UNKNOWN, None, None, how, "no-row")
|
|
788
|
+
if not found: # a macOS table without the reader's own connection is restricted (DEC-0155 RT-1)
|
|
789
|
+
return Owner(UNKNOWN, None, None, how, NO_ROW_RESTRICTED if how in ("sysctl", "netstat") else "no-row")
|
|
804
790
|
if len(found) > 1:
|
|
805
791
|
return Owner(UNKNOWN, None, None, how, "several-rows")
|
|
806
792
|
if how == "proc" and server and found[0].uid is None:
|
|
807
|
-
# Linux: a connection
|
|
808
|
-
#
|
|
809
|
-
#
|
|
810
|
-
# process that accepted it (sock_graft): it answers itself, so a listener swapped after another account
|
|
811
|
-
# accepted the client cannot pass (Project Observatory's 1.2.0 reading, board thread ced00f6e; the contract's
|
|
812
|
-
# CT-254, HA-2's Linux source, released in v0.1.0+2026-11-07.3).
|
|
813
|
-
# A server row its owner already closed (an orphan) also has inode 0 and reads so: no process holds it, so
|
|
814
|
-
# nothing sent to it reaches anyone (0.74.0 unit reading 6).
|
|
793
|
+
# Linux: a connection not accept()ed yet (uid 0, inode 0) belongs to the listener on its local address (exact, else
|
|
794
|
+
# the wildcard) and dies with it; an accepted one's own row carries the accepting process's uid (sock_graft), so a
|
|
795
|
+
# listener swapped after another account accepted cannot pass (CT-254). An orphaned server row reads so too.
|
|
815
796
|
return _one_owner(_listeners_of(rows, found[0].local), how, "not-accepted")
|
|
816
797
|
if how == "proc" and found[0].uid is None:
|
|
817
798
|
# A client's socket with inode 0 has already closed (0.74.0 unit reading 4): never another socket's owner.
|
|
@@ -840,11 +821,32 @@ def _one_owner(found: list[Row], how: str, none_reason: Optional[str] = None) ->
|
|
|
840
821
|
return Owner(UNKNOWN, None, None, how, "several-rows")
|
|
841
822
|
|
|
842
823
|
|
|
824
|
+
_PROOF: list = [None] # fabric_socket_proof once looked for: None not yet, False not beside this file (FAA-292)
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def _proved(ask: str, *args, fallback: Owner, **kwargs) -> Owner:
|
|
828
|
+
"""`fallback`, unless it is a restricted macOS table's `unknown` and `fabric_socket_proof` beside this file finds the
|
|
829
|
+
socket (`accepted`) or the 127.0.0.1 listener (`own_listener`, FAA-272) in this account's processes: else it stands."""
|
|
830
|
+
if fallback.kind != UNKNOWN or not (fallback.reason or "").startswith(TABLE_RESTRICTED) or sys.platform != "darwin":
|
|
831
|
+
return fallback
|
|
832
|
+
try:
|
|
833
|
+
if _PROOF[0] is None:
|
|
834
|
+
sys.path.append(os.path.dirname(os.path.abspath(__file__))) # beside this file, as the kit's modules load
|
|
835
|
+
import fabric_socket_proof
|
|
836
|
+
_PROOF[0] = fabric_socket_proof
|
|
837
|
+
proof = getattr(_PROOF[0], ask)(*args, **kwargs) if _PROOF[0] else None
|
|
838
|
+
except ImportError:
|
|
839
|
+
_PROOF[0], proof = False, None
|
|
840
|
+
except Exception: # noqa: BLE001 - a proof that fails proves nothing; the table's answer stands (fail closed)
|
|
841
|
+
proof = None
|
|
842
|
+
return Owner(THIS_ACCOUNT, _posix_uid(), proof.pid, proof.method, proof.reason) if proof and proof.proved else fallback
|
|
843
|
+
|
|
844
|
+
|
|
843
845
|
def peer_at(server: tuple, client: tuple) -> Owner:
|
|
844
|
-
"""The owner of the CLIENT socket of the connection between `server` and `client`, as a
|
|
845
|
-
|
|
846
|
-
client
|
|
847
|
-
return _owner_of(
|
|
846
|
+
"""The owner of the CLIENT socket of the connection between `server` and `client`, as a server sees them (its own
|
|
847
|
+
address, the peer's): the row whose local address is the client's and whose remote address is the server's."""
|
|
848
|
+
client, server = _address(client), _address(server)
|
|
849
|
+
return _proved("accepted", client, server, wait=False, fallback=_owner_of(client, server))
|
|
848
850
|
|
|
849
851
|
|
|
850
852
|
def peer(sock: socket.socket) -> Owner:
|
|
@@ -857,16 +859,15 @@ def peer(sock: socket.socket) -> Owner:
|
|
|
857
859
|
|
|
858
860
|
|
|
859
861
|
def server_end(sock: socket.socket) -> Owner:
|
|
860
|
-
"""For a client's connected socket: who accepted it. The row whose local address is the
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
On Linux a connection not yet accept()ed has no owner of its own (inode 0): its listener
|
|
864
|
-
answers (the contract's CT-254, HA-2's Linux source)."""
|
|
862
|
+
"""For a client's connected socket: who accepted it. The row whose local address is the server's and whose remote
|
|
863
|
+
address is this socket's own, the server process's accepted socket, so a listener swapped after `listen` cannot
|
|
864
|
+
pass; on Linux a connection not accept()ed yet answers through its listener (CT-254); RT-2 under a restricted table."""
|
|
865
865
|
try:
|
|
866
866
|
own, server = sock.getsockname(), sock.getpeername()
|
|
867
867
|
except OSError as exc:
|
|
868
868
|
return Owner(UNKNOWN, None, None, "none", f"socket: {type(exc).__name__}")
|
|
869
|
-
|
|
869
|
+
server, own = _address(server), _address(own)
|
|
870
|
+
return _proved("accepted", server, own, wait=True, fallback=_owner_of(server, own, server=True))
|
|
870
871
|
|
|
871
872
|
|
|
872
873
|
#: port -> (time.monotonic() of the read, the Owner)
|
|
@@ -887,9 +888,8 @@ def pick_listener(rows: list[Row], port: int) -> list[Row]:
|
|
|
887
888
|
|
|
888
889
|
|
|
889
890
|
def listener(port: int) -> Owner:
|
|
890
|
-
"""Who listens on 127.0.0.1:<port>, or on 0.0.0.0 or ::1, which take the same connections.
|
|
891
|
-
`
|
|
892
|
-
LISTENER_TTL seconds."""
|
|
891
|
+
"""Who listens on 127.0.0.1:<port>, or on 0.0.0.0 or ::1, which take the same connections. `none` when nothing does,
|
|
892
|
+
`unknown` (`several-rows`) when sockets of two owners do. Cached for LISTENER_TTL seconds."""
|
|
893
893
|
now = time.monotonic()
|
|
894
894
|
hit = _LISTENERS.get(port)
|
|
895
895
|
if hit is not None and now - hit[0] < LISTENER_TTL:
|
|
@@ -899,11 +899,19 @@ def listener(port: int) -> Owner:
|
|
|
899
899
|
except TableUnreadable as exc:
|
|
900
900
|
owner = Owner(UNKNOWN, None, None, method(), str(exc))
|
|
901
901
|
else:
|
|
902
|
-
owner = _one_owner(pick_listener(rows, port), how)
|
|
902
|
+
owner = _restricted_listener(rows, port, how) if restricted(rows, how) else _one_owner(pick_listener(rows, port), how)
|
|
903
|
+
owner = _proved("own_listener", port, fallback=owner)
|
|
903
904
|
_LISTENERS[port] = (now, owner)
|
|
904
905
|
return owner
|
|
905
906
|
|
|
906
907
|
|
|
908
|
+
def _restricted_listener(rows: list[Row], port: int, how: str) -> Owner:
|
|
909
|
+
"""DEC-0155 RT-4 (FAA-312): under a restricted table only this account's own bind on exactly 127.0.0.1 is its own."""
|
|
910
|
+
owner = _one_owner([row for row in rows if row.listening and row.local == ("127.0.0.1", port)], how)
|
|
911
|
+
reason = f"{TABLE_RESTRICTED}: no listener of this account on exactly 127.0.0.1:{port}"
|
|
912
|
+
return owner if owner.kind == THIS_ACCOUNT else Owner(UNKNOWN, None, None, how, reason)
|
|
913
|
+
|
|
914
|
+
|
|
907
915
|
def refusal(owner: Owner, port: int, expect_pid: Optional[int] = None) -> Optional[tuple[str, str]]:
|
|
908
916
|
"""(code, why) when a client must not send on a connection whose server end is `owner`."""
|
|
909
917
|
if owner.kind in (ANOTHER_ACCOUNT, SYSTEM):
|
|
@@ -916,29 +924,30 @@ def refusal(owner: Owner, port: int, expect_pid: Optional[int] = None) -> Option
|
|
|
916
924
|
return None
|
|
917
925
|
|
|
918
926
|
|
|
919
|
-
def pid_listens(pid: int, port: int) -> bool:
|
|
920
|
-
"""macOS,
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
if
|
|
927
|
+
def pid_listens(pid: int, port: int, owner: object = THIS_ACCOUNT) -> bool:
|
|
928
|
+
"""macOS, a restricted table (FAA-267, DEC-0155 RT-3): whether process `pid` listens on exactly 127.0.0.1:<port>
|
|
929
|
+
(never the IPv4 wildcard, which `lsof` prints as an IPv6-only one, nor ::1), asked of the process (`lsof -p`). Only
|
|
930
|
+
a caller that started `pid` (a launchd job it installed) vouches with it, never a set-uid one for this account. `owner`:
|
|
931
|
+
the uid `pid` runs as, this account's by default, or None when the caller checks it (a LaunchDaemon's UserName)."""
|
|
932
|
+
if owner is not None and owner != THIS_ACCOUNT and (not isinstance(owner, int) or isinstance(owner, bool) or owner < 0):
|
|
933
|
+
raise ValueError("owner is this-account, a uid or None")
|
|
934
|
+
if sys.platform != "darwin" or not isinstance(pid, int) or isinstance(pid, bool) or pid <= 1 or (
|
|
935
|
+
owner == THIS_ACCOUNT and _set_uid()):
|
|
925
936
|
return False
|
|
926
937
|
try:
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
938
|
+
if owner is not None:
|
|
939
|
+
uid = parse_ps_uid(_run([PS, "-o", "uid=", "-p", str(pid)]))
|
|
940
|
+
if uid is None or uid != (_posix_uid() if owner == THIS_ACCOUNT else owner):
|
|
941
|
+
return False
|
|
930
942
|
out = _run([LSOF, "-nP", "-a", "-p", str(pid), "-i4TCP", "-sTCP:LISTEN", "-Fn"])
|
|
931
943
|
except TableUnreadable:
|
|
932
944
|
return False
|
|
933
|
-
return any(host
|
|
945
|
+
return any(host == "127.0.0.1" and p == port for host, p in parse_lsof_listeners(out))
|
|
934
946
|
|
|
935
947
|
|
|
936
948
|
class CheckedHTTPConnection(http.client.HTTPConnection):
|
|
937
|
-
"""An `http.client.HTTPConnection` whose every `connect()`
|
|
938
|
-
|
|
939
|
-
`Connection: close` or the caller closed the connection, and that new connection may reach a
|
|
940
|
-
different listener; so the check is in `connect()`, not once in `own_connection`. A refused
|
|
941
|
-
connect closes the socket and raises ListenerRefused before any byte of a request is sent."""
|
|
949
|
+
"""An `http.client.HTTPConnection` whose every `connect()` (the reconnect `auto_open` makes too, which may reach
|
|
950
|
+
another listener) checks the server end: refused, it closes the socket and raises ListenerRefused before a byte."""
|
|
942
951
|
|
|
943
952
|
def __init__(self, host: str, port: int, *, expect_pid: Optional[int] = None, timeout: float = 10.0,
|
|
944
953
|
vouch_pid: Optional[int] = None) -> None:
|
|
@@ -953,7 +962,7 @@ class CheckedHTTPConnection(http.client.HTTPConnection):
|
|
|
953
962
|
super().connect()
|
|
954
963
|
owner = server_end(self.sock)
|
|
955
964
|
if (owner.kind == UNKNOWN and (owner.reason or "").startswith(TABLE_RESTRICTED) and getattr(self, "vouch_pid", None)
|
|
956
|
-
and pid_listens(self.vouch_pid, self.port)):
|
|
965
|
+
and _address(self.sock.getpeername())[0] == "127.0.0.1" and pid_listens(self.vouch_pid, self.port)):
|
|
957
966
|
# The table hides other processes (FAA-267); the process this caller started holds the listener.
|
|
958
967
|
owner = Owner(THIS_ACCOUNT, _posix_uid(), self.vouch_pid, "lsof-pid")
|
|
959
968
|
refused = refusal(owner, self.port, self.expect_pid)
|
|
@@ -964,19 +973,28 @@ class CheckedHTTPConnection(http.client.HTTPConnection):
|
|
|
964
973
|
|
|
965
974
|
def own_connection(port: int, *, expect_pid: Optional[int] = None, timeout: float = 10.0,
|
|
966
975
|
host: str = "127.0.0.1", vouch_pid: Optional[int] = None) -> CheckedHTTPConnection:
|
|
967
|
-
"""A `CheckedHTTPConnection` to `host:port`, connected, whose server end is this account's
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
server-not-own another account's or the system's process accepted it
|
|
974
|
-
server-unverified its owner could not be told (nothing is sent: fail closed)
|
|
975
|
-
server-not-this-workspace this account's process, but not the pid expected
|
|
976
|
-
|
|
977
|
-
`vouch_pid`, as on `CheckedHTTPConnection` and the Node twin's `checkedSocket`: a pid the caller started
|
|
978
|
-
(FAA-267).
|
|
979
|
-
"""
|
|
976
|
+
"""A `CheckedHTTPConnection` to `host:port`, connected, whose server end is this account's (and, with `expect_pid`,
|
|
977
|
+
that process's), checked on the very connection the caller sends on and on every reconnect; no proxy is consulted.
|
|
978
|
+
ListenerRefused, the connection closed: `server-not-own` (another account's or the system's process accepted it),
|
|
979
|
+
`server-unverified` (its owner could not be told: fail closed), `server-not-this-workspace` (not the pid expected).
|
|
980
|
+
`vouch_pid`, as on `CheckedHTTPConnection` and the Node twin's `checkedSocket`: a pid the caller started (FAA-267)."""
|
|
980
981
|
conn = CheckedHTTPConnection(host, port, expect_pid=expect_pid, timeout=timeout, vouch_pid=vouch_pid)
|
|
981
982
|
conn.connect()
|
|
982
983
|
return conn
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
def main(argv: Optional[list] = None) -> int:
|
|
987
|
+
"""`listener <port> [--json]`: the kind of who listens on 127.0.0.1:<port>, which the dashboard link gate takes as
|
|
988
|
+
`listener_owner` (FAA-315), or the whole owner as JSON; exit 2 on a usage error."""
|
|
989
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
990
|
+
as_json = args[2:] == ["--json"]
|
|
991
|
+
if len(args) != 2 + as_json or args[0] != "listener" or not _ascii_digits(args[1]) or not 0 < int(args[1]) < 65536:
|
|
992
|
+
print("usage: fabric_peer_owner.py listener <port> [--json]", file=sys.stderr)
|
|
993
|
+
return 2
|
|
994
|
+
owner = listener(int(args[1]))
|
|
995
|
+
print(json.dumps(owner.as_dict()) if as_json else owner.kind)
|
|
996
|
+
return 0
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
if __name__ == "__main__":
|
|
1000
|
+
sys.exit(main())
|