netbeagle 0.1.0 → 0.2.0
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/README.md +58 -25
- package/dist/netbeagle.js +64 -28
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -10,15 +10,16 @@
|
|
|
10
10
|
<a href="https://www.npmjs.com/package/netbeagle"><img src="https://img.shields.io/npm/v/netbeagle?color=a8521a" alt="npm version"></a>
|
|
11
11
|
<a href="https://github.com/michelsalib/netbeagle/actions/workflows/ci.yml"><img src="https://github.com/michelsalib/netbeagle/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
12
12
|
<img src="https://img.shields.io/node/v/netbeagle" alt="Node version">
|
|
13
|
-
<img src="https://img.shields.io/badge/platform-Linux%20%7C%
|
|
13
|
+
<img src="https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20Windows-blue" alt="Linux, macOS and Windows">
|
|
14
14
|
<a href="LICENSE"><img src="https://img.shields.io/npm/l/netbeagle" alt="MIT license"></a>
|
|
15
15
|
<a href="https://scorecard.dev/viewer/?uri=github.com/michelsalib/netbeagle"><img src="https://api.scorecard.dev/projects/github.com/michelsalib/netbeagle/badge" alt="OpenSSF Scorecard"></a>
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
What is on port 5432, a local postgres or the one in docker? Can my laptop's neighbours reach it, or only
|
|
19
19
|
this machine? Which process keeps a connection open to that server, and through which interface and
|
|
20
|
-
gateway does it leave? netbeagle answers those in one command, by reading what the
|
|
21
|
-
knows (`/proc
|
|
20
|
+
gateway does it leave? netbeagle answers those in one command, by reading what the system already
|
|
21
|
+
knows (`/proc` and `/sys` on Linux, `netstat` and `lsof` on macOS, PowerShell on Windows) and asking
|
|
22
|
+
docker which container sits behind each published port.
|
|
22
23
|
|
|
23
24
|
**Try it, nothing to install:**
|
|
24
25
|
|
|
@@ -68,8 +69,8 @@ dns 127.0.0.53 (local resolver)
|
|
|
68
69
|
|
|
69
70
|
## Install
|
|
70
71
|
|
|
71
|
-
netbeagle is a single JavaScript file that runs on Node ≥ 20 or Bun, on Linux
|
|
72
|
-
the
|
|
72
|
+
netbeagle is a single JavaScript file that runs on Node ≥ 20 or Bun, on Linux (WSL included), macOS and
|
|
73
|
+
Windows. Without either, the installers fetch a standalone binary instead, which needs nothing.
|
|
73
74
|
|
|
74
75
|
```sh
|
|
75
76
|
npx netbeagle map # or: bunx netbeagle map, nothing to install
|
|
@@ -80,16 +81,32 @@ curl -fsSL https://github.com/michelsalib/netbeagle/releases/latest/download/ins
|
|
|
80
81
|
To pin a version, or to read the script before running it:
|
|
81
82
|
|
|
82
83
|
```sh
|
|
83
|
-
curl -fsSL https://github.com/michelsalib/netbeagle/releases/download/v0.
|
|
84
|
+
curl -fsSL https://github.com/michelsalib/netbeagle/releases/download/v0.2.0/install.sh -o install.sh
|
|
84
85
|
less install.sh && sh install.sh
|
|
85
86
|
gh attestation verify install.sh -R michelsalib/netbeagle # optional: proves GitHub Actions built it
|
|
86
87
|
```
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
`SHA256SUMS` and attested like the rest; see [SECURITY.md](SECURITY.md#verifying-a-release).
|
|
89
|
+
On Windows, in PowerShell:
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
```powershell
|
|
92
|
+
irm https://github.com/michelsalib/netbeagle/releases/latest/download/install.ps1 | iex
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`install.sh` puts `netbeagle` in `~/.local/bin` (`NETBEAGLE_INSTALL_DIR` to change it, `NETBEAGLE_VERSION=0.2.0`
|
|
96
|
+
to pin a version), checks it against the release's `SHA256SUMS`, and runs it on Node, or on Bun when there
|
|
97
|
+
is no Node ≥ 20. With neither, or with `NETBEAGLE_BINARY=1`, it installs the standalone binary for your
|
|
98
|
+
system (Linux and macOS, x64 and arm64). `install.ps1` does the same on Windows, in
|
|
99
|
+
`%LOCALAPPDATA%\Programs\netbeagle`: `netbeagle.mjs` and a `netbeagle.cmd` launcher, or `netbeagle.exe`.
|
|
100
|
+
Neither edits your `PATH`: they print what to run.
|
|
101
|
+
|
|
102
|
+
The binaries are release files too (`netbeagle-linux-x64`, `netbeagle-linux-arm64`, `netbeagle-darwin-x64`,
|
|
103
|
+
`netbeagle-darwin-arm64`, `netbeagle-windows-x64.exe`), listed in `SHA256SUMS` and attested like the rest;
|
|
104
|
+
see [SECURITY.md](SECURITY.md#verifying-a-release).
|
|
105
|
+
|
|
106
|
+
On Linux and macOS, without root, netbeagle can't see other users' processes, so their sockets show no
|
|
107
|
+
process; `sudo netbeagle` shows them all. Windows names every socket's process, but shows other users'
|
|
108
|
+
command lines only in an administrator terminal. netbeagle only reads: see
|
|
109
|
+
[what it does with your machine](SECURITY.md#what-netbeagle-does-with-your-machine).
|
|
93
110
|
|
|
94
111
|
## Commands
|
|
95
112
|
|
|
@@ -110,21 +127,36 @@ lines are shown with the values of `--token=`, `--password` and the like masked
|
|
|
110
127
|
|
|
111
128
|
## How it works
|
|
112
129
|
|
|
113
|
-
|
|
130
|
+
Each system is read its own way, into the same picture, so every command works the same everywhere.
|
|
131
|
+
|
|
132
|
+
- **Linux**: sockets from `/proc/net/tcp`, `tcp6`, `udp` and `udp6`; IPv4 clients of a socket bound to `::`
|
|
114
133
|
(`::ffff:1.2.3.4`) are shown as IPv4. Each socket's process is found through the `socket:[inode]` links
|
|
115
134
|
in `/proc/PID/fd`. Without root, other users' processes can't be read: their sockets show the user
|
|
116
|
-
only, and `sudo netbeagle` shows them all.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
135
|
+
only, and `sudo netbeagle` shows them all. Interfaces from `/sys/class/net` (kind, state, bridge and
|
|
136
|
+
its ports, traffic) and their addresses from `getifaddrs`; routes from the kernel's main table,
|
|
137
|
+
`/proc/net/route` and `/proc/net/ipv6_route`.
|
|
138
|
+
- **macOS**: every user's sockets from `netstat -an` (`tcp46` for a socket taking both families), and
|
|
139
|
+
the process behind each from `lsof`: without root, only your own, so other users' show no process and
|
|
140
|
+
`sudo netbeagle` shows them all. Interfaces from `ifconfig -a` (bridge members included), their traffic
|
|
141
|
+
from `netstat -ibn`, which one is Wi-Fi from `networksetup`; routes from `netstat -rn`, without the
|
|
142
|
+
ARP entries and interface-scoped copies; processes from `ps`.
|
|
143
|
+
- **Windows**: one PowerShell call. Sockets and their processes from `Get-NetTCPConnection` and
|
|
144
|
+
`Get-NetUDPEndpoint` (`netstat` translates its states into the system's language; these don't),
|
|
145
|
+
interfaces, addresses, traffic and DNS servers from .NET's `NetworkInterface`, routes from `Get-NetRoute`
|
|
146
|
+
(their metric plus the interface's, as Windows compares them), command lines from `Win32_Process`.
|
|
147
|
+
It takes a couple of seconds, PowerShell's start included.
|
|
148
|
+
- `route` picks the longest matching prefix, then the lowest metric, as the system does (policy
|
|
149
|
+
routing, `ip rule` on Linux, is not replayed).
|
|
121
150
|
- **Containers**: `docker inspect` of the running containers gives their networks (and the host bridge
|
|
122
151
|
each uses, `docker0` or `br-<network id>`) and their published ports. A published port belongs to
|
|
123
152
|
its container, whether `docker-proxy` holds a socket for it or the firewall forwards it with none.
|
|
153
|
+
- **Containers on macOS and Windows**: Docker Desktop runs them in a VM, so their networks are not on this
|
|
154
|
+
machine; a published port shows its container all the same.
|
|
124
155
|
- **Who reaches what**: a socket on `127.0.0.1`, `::1` or another address of the loopback is reachable
|
|
125
|
-
from this machine only; on `0.0.0.0` or `::`, from every interface
|
|
126
|
-
|
|
127
|
-
|
|
156
|
+
from this machine only; on `0.0.0.0` or `::`, from every interface; on one address, from the interface
|
|
157
|
+
holding it. A socket on `::` takes IPv4 too by default on Linux (`net.ipv6.bindv6only` is 0) and on
|
|
158
|
+
macOS (`tcp46`), and IPv6 only on Windows unless its program asks for both: those say
|
|
159
|
+
`every interface, IPv6 only`. Firewall rules are not taken into account.
|
|
128
160
|
- **WSL**: Windows forwards its own `localhost` to ports bound to `127.0.0.1` or every interface in the
|
|
129
161
|
WSL VM, so those say `+ Windows`.
|
|
130
162
|
- Connections between two processes of this machine are matched end to end, so `curl 400 → :5000 node
|
|
@@ -132,9 +164,9 @@ lines are shown with the values of `--token=`, `--password` and the like masked
|
|
|
132
164
|
|
|
133
165
|
## Status
|
|
134
166
|
|
|
135
|
-
Linux,
|
|
136
|
-
|
|
137
|
-
|
|
167
|
+
Linux (WSL included), macOS and Windows. Planned: `netbeagle doctor` (services exposed on every
|
|
168
|
+
interface, ports bound twice, DNS servers with no route), firewall rules, network namespaces (inside
|
|
169
|
+
containers), and a Homebrew tap and a Scoop bucket.
|
|
138
170
|
|
|
139
171
|
## The pack
|
|
140
172
|
|
|
@@ -151,14 +183,15 @@ nothing to configure.
|
|
|
151
183
|
```sh
|
|
152
184
|
bun install
|
|
153
185
|
bun run dev -- map # run from source
|
|
154
|
-
bun test # unit tests on
|
|
186
|
+
bun test # unit tests on the fixtures of each system, plus the built CLI on node on this machine
|
|
155
187
|
bun run typecheck
|
|
156
188
|
bun run lint # Biome: formatting and lint
|
|
157
189
|
bun run build # dist/netbeagle.js, plain JS for node >= 20
|
|
158
190
|
```
|
|
159
191
|
|
|
160
|
-
`test/fixtures/root` is a small machine as `/proc`, `/sys` and `/etc` show it, written by
|
|
161
|
-
`scripts/fixture.py` (its docstring describes it)
|
|
192
|
+
`test/fixtures/root` is a small Linux machine as `/proc`, `/sys` and `/etc` show it, written by
|
|
193
|
+
`scripts/fixture.py` (its docstring describes it); `test/fixtures/macos` holds what its commands print
|
|
194
|
+
on a Mac, and `test/fixtures/windows` what netbeagle's PowerShell script prints on Windows. Source uses only `node:` APIs, so the build runs on
|
|
162
195
|
both Node and Bun.
|
|
163
196
|
|
|
164
197
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for pull requests and releases.
|
package/dist/netbeagle.js
CHANGED
|
@@ -1,19 +1,55 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{lookup as Nt}from"node:dns/promises";import{isIP as Et}from"node:net";import{parseArgs as Ct}from"node:util";var E={name:"netbeagle",version:"0.1.0",description:"See which process listens on each port, which interfaces reach it, and how everything on your machine connects",keywords:["network","ports","netstat","ss","lsof","interfaces","docker","routes"],license:"MIT",author:"Michel Salib",repository:{type:"git",url:"git+https://github.com/michelsalib/netbeagle.git"},homepage:"https://github.com/michelsalib/netbeagle#readme",bugs:"https://github.com/michelsalib/netbeagle/issues",type:"module",bin:{netbeagle:"dist/netbeagle.js"},files:["dist/netbeagle.js"],engines:{node:">=20"},scripts:{dev:"bun src/cli.ts",build:"bun build src/cli.ts --target=node --minify --outfile dist/netbeagle.js","build:bin":"bun scripts/build-binaries.ts",test:"bun test --timeout 30000",typecheck:"tsc",lint:"biome check",prepublishOnly:"bun run typecheck && bun run lint && bun run build && bun test --timeout 30000"},devDependencies:{"@biomejs/biome":"2.5.15","@types/bun":"^1.4.2","@types/node":"^26.6.4",typescript:"^7.0.2"}};import{isIPv4 as fe,isIPv6 as me}from"node:net";function K(e){if(fe(e))return e.split(".").map(Number);if(!me(e))return;let t=e.replace(/%.*$/,""),n=t.match(/(\d+\.\d+\.\d+\.\d+)$/);if(n?.[1]){let[a,d,u,p]=n[1].split(".").map(Number);t=`${t.slice(0,-n[1].length)}${(a<<8|d).toString(16)}:${(u<<8|p).toString(16)}`}let[o="",s]=t.split("::"),r=o?o.split(":"):[],i=s?s.split(":"):[];return(s===void 0?r:[...r,...Array(8-r.length-i.length).fill("0"),...i]).flatMap((a)=>{let d=Number.parseInt(a,16);return[d>>8,d&255]})}var he=(e)=>fe(e)?4:me(e)?6:void 0;function Y(e){let t=[];for(let r=0;r<16;r+=2)t.push((e[r]??0)<<8|(e[r+1]??0));let n=-1,o=1;for(let r=0;r<8;){if(t[r]!==0){r++;continue}let i=r;while(i<8&&t[i]===0)i++;if(i-r>o)n=r,o=i-r;r=i}let s=t.map((r)=>r.toString(16));if(n<0)return s.join(":");return`${s.slice(0,n).join(":")}::${s.slice(n+o).join(":")}`}function z(e){let t=Number.parseInt(e,16);return[t&255,t>>>8&255,t>>>16&255,t>>>24&255]}var S=(e)=>z(e).join(".");function ge(e){let t=[];for(let n=0;n<32;n+=8)t.push(...z(e.slice(n,n+8)));if(t.slice(0,10).every((n)=>n===0)&&t[10]===255&&t[11]===255)return{family:4,address:t.slice(12).join(".")};return{family:6,address:Y(t)}}function q(e){let t=[];for(let n=0;n<32;n+=2)t.push(Number.parseInt(e.slice(n,n+2),16));return Y(t)}function X(e,t,n){let o=K(e),s=K(t);if(!o||!s||o.length!==s.length)return!1;for(let r=0;r<n;r++){let i=r>>3,c=128>>(r&7);if(((o[i]??0)&c)!==((s[i]??0)&c))return!1}return!0}function be(e){let n=(K(e.address)??[]).map((o,s)=>{let r=Math.max(0,Math.min(8,e.prefix-s*8));return o&(255<<8-r&255)});return e.family===4?n.join("."):Y(n)}var k=(e)=>e==="0.0.0.0"||e==="::",v=(e)=>e==="::1"||e.startsWith("127."),ke=(e)=>e.startsWith("169.254.")||/^fe[89ab][0-9a-f]:/i.test(e),I=(e)=>e.address.includes(":")?`[${e.address}]:${e.port}`:`${e.address}:${e.port}`,we=(e)=>z(e).reduce((t,n)=>t+n.toString(2).replaceAll("0","").length,0);var Q=(e,t)=>e.users[t]??String(t),Z=(e,t)=>t===void 0?void 0:e.processes.find((n)=>n.pid===t),C=(e,t)=>e.interfaces.find((n)=>n.addresses.some((o)=>o.address===t));function ye(e,t){for(let n of e.containers)for(let o of n.networks)if(o.ip===t)return{container:n,network:o};return}function j(e,t){let n=he(t)??4,o={dest:t,family:n};if(v(t))return{...o,kind:"loopback",iface:"lo",source:t};let s=C(e,t);if(s)return{...o,kind:"local",iface:s.name,source:t};let r=e.routes.filter((p)=>p.family===n&&X(t,p.dest,p.prefix)).sort((p,f)=>f.prefix-p.prefix||p.metric-f.metric)[0];if(!r)return{...o,kind:"unreachable"};let i=e.interfaces.find((p)=>p.name===r.iface),c=r.gateway??t,a=i?.addresses.filter((p)=>p.family===n)??[],d=a.find((p)=>X(c,be(p),p.prefix))?.address??a.find((p)=>!ke(p.address))?.address??a[0]?.address,u=ye(e,t);return{...o,kind:u?"container":r.gateway?"gateway":"network",iface:r.iface,gateway:r.gateway,route:r,source:d,container:u?.container}}function ot(e,t){let n=(r)=>v(r)||C(e,r)?.kind==="loopback";if(t.every(n))return{scope:"local",interfaces:["lo"]};let o=e.interfaces.filter((r)=>r.state!=="down"&&r.addresses.length);if(t.some(k)){let r=new Set(t.some((i)=>i==="::")?[4,6]:[4]);return{scope:"everywhere",interfaces:o.filter((i)=>!i.master).filter((i)=>i.addresses.some((c)=>r.has(c.family))).map((i)=>i.name)}}let s=new Set;for(let r of t){if(v(r))s.add("lo");let i=C(e,r);if(i)s.add(i.name)}return{scope:"interfaces",interfaces:[...s]}}var rt=(e,t,n,o)=>e.proto===t&&e.hostPort===n&&(o===void 0||k(e.hostIp)||k(o)||e.hostIp===o);function st(e,t,n,o){for(let s of e.containers)for(let r of s.ports)if(rt(r,t,n,o))return{container:s,published:r};return}var $e=(e)=>e.state==="LISTEN"||e.state==="UNCONN"&&e.local.port!==0;function R(e){let t=new Map;for(let o of e.sockets.filter($e)){let s=st(e,o.proto,o.local.port,o.local.address),r=s?`c${s.container.id}`:o.pid!==void 0?`p${o.pid}`:`u${o.uid}`,i=`${o.proto}/${o.local.port}/${r}`,c=t.get(i);if(!c)c={proto:o.proto,port:o.local.port,addresses:[],reach:{scope:"local",interfaces:[]},process:Z(e,o.pid),user:Q(e,o.uid),container:s?.container,published:s?.published,sockets:[],clients:[]},t.set(i,c);if(c.sockets.push(o),!c.addresses.includes(o.local.address))c.addresses.push(o.local.address)}for(let o of e.containers)for(let s of o.ports){if([...t.values()].some((a)=>a.proto===s.proto&&a.port===s.hostPort&&a.container?.id===o.id))continue;let i=`${s.proto}/${s.hostPort}/c${o.id}`,c=t.get(i);if(c){if(!c.addresses.includes(s.hostIp))c.addresses.push(s.hostIp);continue}t.set(i,{proto:s.proto,port:s.hostPort,addresses:[s.hostIp],reach:{scope:"local",interfaces:[]},user:"root",container:o,published:s,sockets:[],clients:[]})}let n=[...t.values()];for(let o of n)o.reach=ot(e,o.addresses);for(let o of P(e,n))if(o.direction==="in")O(n,o.proto,o.local)?.clients.push(o);return n.sort((o,s)=>o.port-s.port||o.proto.localeCompare(s.proto))}function O(e,t,n){return e.find((o)=>o.proto===t&&o.port===n.port&&o.addresses.some((s)=>s===n.address||k(s)))}function P(e,t){let n=t??R(e),o=[];for(let s of e.sockets){if($e(s)||s.remote.port===0)continue;let r=O(n,s.proto,s.local)?"in":"out";o.push({proto:s.proto,local:s.local,remote:s.remote,state:s.state,process:Z(e,s.pid),user:s.inode===0?void 0:Q(e,s.uid),direction:r,peer:it(e,n,s)})}return o}function it(e,t,n){let o=n.remote.address;if(v(o)||C(e,o)){let r=e.sockets.find((c)=>c!==n&&c.proto===n.proto&&c.local.port===n.remote.port&&c.remote.port===n.local.port&&c.local.address===o&&c.remote.address===n.local.address),i=O(t,n.proto,n.remote);return{kind:"local",process:Z(e,r?.pid),user:r?Q(e,r.uid):void 0,listener:i&&O(t,n.proto,n.local)!==i?i:void 0}}let s=ye(e,o);if(s)return{kind:"container",container:s.container,network:s.network.name};return{kind:"remote",route:j(e,o)}}function xe(e,t){return{port:t,listeners:R(e).filter((n)=>n.port===t),outbound:P(e).filter((n)=>n.direction==="out"&&n.remote.port===t&&!(n.peer.kind==="local"&&n.peer.listener?.port===t))}}function ee(e){let t=R(e),n=P(e,t).filter((r)=>r.state==="ESTABLISHED"),o=[],s=[];for(let r of e.interfaces){if(r.master)continue;let i=e.containers.flatMap((d)=>d.networks.filter((u)=>u.bridge===r.name).map((u)=>({container:d,ip:u.ip}))),c={iface:r,routes:e.routes.filter((d)=>d.iface===r.name),listeners:t.filter((d)=>d.reach.scope!=="everywhere"&&d.reach.interfaces.includes(r.name)),outbound:n.filter((d)=>d.direction==="out"&&d.peer.kind==="remote"&&d.peer.route.iface===r.name),local:r.kind==="loopback"?n.filter((d)=>d.peer.kind==="local"&&d.direction==="out"):[],containers:i},a=!r.addresses.length&&!c.listeners.length&&!c.outbound.length&&!i.length;if(r.state==="down"&&a)s.push(r.name);else o.push(c)}return{hostname:e.hostname,environment:e.environment,everywhere:t.filter((r)=>r.reach.scope==="everywhere"),interfaces:o,idle:s,dns:e.dns.map((r)=>({server:r,route:j(e,r)})),hidden:e.hidden}}var Se=(e)=>e.sockets.filter((t)=>t.pid===void 0&&t.uid!==e.uid).length;import{readFileSync as $t}from"node:fs";import{hostname as xt}from"node:os";import{join as B}from"node:path";import{spawnSync as ct}from"node:child_process";function at(e){let t;try{t=JSON.parse(e)}catch{return[]}return t.map((n)=>{let o=[];for(let[s,r]of Object.entries(n.NetworkSettings?.Ports??{})){let[i="0",c="tcp"]=s.split("/");for(let a of r??[])o.push({proto:c,hostIp:a.HostIp||"0.0.0.0",hostPort:Number(a.HostPort),containerPort:Number(i)})}return{id:(n.Id??"").slice(0,12),name:(n.Name??"").replace(/^\//,""),image:n.Config?.Image??"",project:n.Config?.Labels?.["com.docker.compose.project"],networks:Object.entries(n.NetworkSettings?.Networks??{}).map(([s,r])=>({name:s,bridge:s==="bridge"?"docker0":r.NetworkID&&!["host","none"].includes(s)?`br-${r.NetworkID.slice(0,12)}`:void 0,ip:r.IPAddress||r.GlobalIPv6Address||void 0})),ports:o}})}function ve(){let e=(o)=>ct("docker",o,{encoding:"utf8",timeout:5000}),t=e(["ps","-q"]);if(t.status!==0||!t.stdout.trim())return[];let n=e(["inspect",...t.stdout.trim().split(/\s+/)]);return n.status===0?at(n.stdout):[]}import{existsSync as L,readdirSync as te,readFileSync as dt,readlinkSync as ut}from"node:fs";import{networkInterfaces as pt}from"node:os";import{basename as Ie,join as b}from"node:path";function Re(){let e={};for(let[t,n]of Object.entries(pt()))e[t]=(n??[]).map((o)=>({family:o.family==="IPv6"?6:4,address:o.address.replace(/%.*$/,""),prefix:Number(o.cidr?.split("/")[1]??(o.family==="IPv6"?128:32))}));return e}function Pe(e,t){let n=b(e,"class/net"),o=[];try{o=te(n)}catch{}let s=o.map((r)=>{let i=b(n,r),c=(m)=>{try{return dt(b(i,m),"utf8").trim()}catch{return""}},a=(m)=>Number(c(m))||0,d;try{d=Ie(ut(b(i,"master")))}catch{}let u=[];try{u=te(b(i,"brif"))}catch{u=te(i).filter((m)=>m.startsWith("lower_")).map((m)=>m.slice(6))}let p=c("operstate"),f=c("address");return{name:r,index:a("ifindex"),kind:lt(i,c),state:p==="up"?"up":p==="down"?"down":"unknown",mtu:a("mtu"),mac:f&&f!=="00:00:00:00:00:00"?f:void 0,addresses:t[r]??[],master:d,members:u.sort(),rxBytes:a("statistics/rx_bytes"),txBytes:a("statistics/tx_bytes")}});for(let r of s)if(r.kind==="loopback"&&r.state==="unknown"&&r.addresses.length)r.state="up";return s.sort((r,i)=>r.index-i.index)}function lt(e,t){let n=t("type");if(n==="772")return"loopback";let o=t("uevent").match(/^DEVTYPE=(.*)$/m)?.[1];if(o==="wlan"||L(b(e,"wireless")))return"wifi";if(o==="bridge"||L(b(e,"bridge")))return"bridge";if(o==="vlan")return"vlan";if(o==="bond"||L(b(e,"bonding")))return"bond";if(o==="wireguard"||n==="65534")return o==="wireguard"?"wireguard":"tun";if(Ie(e).startsWith("veth"))return"veth";if(L(b(e,"device")))return"ethernet";return"virtual"}import{readdirSync as Te,readFileSync as Ne,readlinkSync as ft}from"node:fs";import{join as A}from"node:path";function Ee(e){let t=[],n=new Map,o=0,s=[];try{s=Te(e).filter((r)=>/^\d+$/.test(r))}catch{}s.sort((r,i)=>Number(r)-Number(i));for(let r of s){let i=Number(r),c=A(e,r),a;try{a=Te(A(c,"fd"))}catch{o++;continue}let d=0;for(let u of a){let p;try{p=ft(A(c,"fd",u))}catch{continue}let f=p.match(/^socket:\[(\d+)\]$/);if(!f)continue;d++;let m=Number(f[1]);if(!n.has(m))n.set(m,i)}if(d===0)continue;t.push(mt(c,i))}return{processes:t,owners:n,hidden:o}}function mt(e,t){let n=(d)=>{try{return Ne(A(e,d),"utf8")}catch{return""}},o=n("comm").trim(),s=n("cmdline").split("\x00").filter(Boolean),r=s[0]?.split("/").pop(),i=(o&&s.join(" ").includes(o)?o:r||o)||String(t),c=s.join(" ")||`[${i}]`,a=Number(n("status").match(/^Uid:\s+(\d+)/m)?.[1]??-1);return{pid:t,name:i,cmd:c,uid:a}}function Ce(e){let t={},n="";try{n=Ne(e,"utf8")}catch{}for(let o of n.split(`
|
|
3
|
-
`)){let
|
|
4
|
-
`).slice(1))
|
|
5
|
-
`)){let
|
|
6
|
-
`)
|
|
7
|
-
`)
|
|
8
|
-
`),
|
|
9
|
-
`).
|
|
10
|
-
`)
|
|
11
|
-
`).
|
|
12
|
-
`)
|
|
13
|
-
`).
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
2
|
+
import{lookup as an}from"node:dns/promises";import{isIP as dn}from"node:net";import{parseArgs as un}from"node:util";var O={name:"netbeagle",version:"0.2.0",description:"See which process listens on each port, which interfaces reach it, and how everything on your machine connects",keywords:["network","ports","netstat","ss","lsof","interfaces","docker","routes","linux","macos","windows"],license:"MIT",author:"Michel Salib",repository:{type:"git",url:"git+https://github.com/michelsalib/netbeagle.git"},homepage:"https://github.com/michelsalib/netbeagle#readme",bugs:"https://github.com/michelsalib/netbeagle/issues",type:"module",bin:{netbeagle:"dist/netbeagle.js"},files:["dist/netbeagle.js"],engines:{node:">=20"},scripts:{dev:"bun src/cli.ts",build:"bun build src/cli.ts --target=node --minify --outfile dist/netbeagle.js","build:bin":"bun scripts/build-binaries.ts",test:"bun test --timeout 30000",typecheck:"tsc",lint:"biome check",prepublishOnly:"bun run typecheck && bun run lint && bun run build && bun test --timeout 30000"},devDependencies:{"@biomejs/biome":"2.5.15","@types/bun":"^1.4.2","@types/node":"^26.6.4",typescript:"^7.0.2"}};import{isIPv4 as be,isIPv6 as ke}from"node:net";function z(e){if(be(e))return e.split(".").map(Number);if(!ke(e))return;let t=e.replace(/%.*$/,""),o=t.match(/(\d+\.\d+\.\d+\.\d+)$/);if(o?.[1]){let[a,u,p,d]=o[1].split(".").map(Number);t=`${t.slice(0,-o[1].length)}${(a<<8|u).toString(16)}:${(p<<8|d).toString(16)}`}let[n="",s]=t.split("::"),r=n?n.split(":"):[],c=s?s.split(":"):[];return(s===void 0?r:[...r,...Array(8-r.length-c.length).fill("0"),...c]).flatMap((a)=>{let u=Number.parseInt(a,16);return[u>>8,u&255]})}var we=(e)=>be(e)?4:ke(e)?6:void 0;function X(e){let t=[];for(let r=0;r<16;r+=2)t.push((e[r]??0)<<8|(e[r+1]??0));let o=-1,n=1;for(let r=0;r<8;){if(t[r]!==0){r++;continue}let c=r;while(c<8&&t[c]===0)c++;if(c-r>n)o=r,n=c-r;r=c}let s=t.map((r)=>r.toString(16));if(o<0)return s.join(":");return`${s.slice(0,o).join(":")}::${s.slice(o+n).join(":")}`}function Q(e){let t=Number.parseInt(e,16);return[t&255,t>>>8&255,t>>>16&255,t>>>24&255]}var P=(e)=>Q(e).join(".");function $e(e){let t=[];for(let o=0;o<32;o+=8)t.push(...Q(e.slice(o,o+8)));if(t.slice(0,10).every((o)=>o===0)&&t[10]===255&&t[11]===255)return{family:4,address:t.slice(12).join(".")};return{family:6,address:X(t)}}function Z(e){let t=[];for(let o=0;o<32;o+=2)t.push(Number.parseInt(e.slice(o,o+2),16));return X(t)}function ee(e,t,o){let n=z(e),s=z(t);if(!n||!s||n.length!==s.length)return!1;for(let r=0;r<o;r++){let c=r>>3,i=128>>(r&7);if(((n[c]??0)&i)!==((s[c]??0)&i))return!1}return!0}function ye(e){let o=(z(e.address)??[]).map((n,s)=>{let r=Math.max(0,Math.min(8,e.prefix-s*8));return n&(255<<8-r&255)});return e.family===4?o.join("."):X(o)}function b(e){let t=e.replace(/%.*$/,"").toLowerCase(),o=t.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);if(o?.[1])return{family:4,address:o[1]};return{family:t.includes(":")?6:4,address:t}}var S=(e)=>e==="0.0.0.0"||e==="::",N=(e)=>e==="::1"||e.startsWith("127."),xe=(e)=>e.startsWith("169.254.")||/^fe[89ab][0-9a-f]:/i.test(e),T=(e)=>e.address.includes(":")?`[${e.address}]:${e.port}`:`${e.address}:${e.port}`,Se=(e)=>Q(e).reduce((t,o)=>t+o.toString(2).replaceAll("0","").length,0);var ne=(e,t)=>t===void 0?void 0:e.users[t]??String(t),te=(e)=>e.interfaces.find((t)=>t.kind==="loopback")?.name??"lo",oe=(e,t)=>t===void 0?void 0:e.processes.find((o)=>o.pid===t),L=(e,t)=>e.interfaces.find((o)=>o.addresses.some((n)=>n.address===t));function Ie(e,t){for(let o of e.containers)for(let n of o.networks)if(n.ip===t)return{container:o,network:n};return}function A(e,t){let o=we(t)??4,n={dest:t,family:o};if(N(t))return{...n,kind:"loopback",iface:te(e),source:t};let s=L(e,t);if(s)return{...n,kind:"local",iface:s.name,source:t};let r=e.routes.filter((d)=>d.family===o&&ee(t,d.dest,d.prefix)).sort((d,l)=>l.prefix-d.prefix||d.metric-l.metric)[0];if(!r)return{...n,kind:"unreachable"};let c=e.interfaces.find((d)=>d.name===r.iface),i=r.gateway??t,a=c?.addresses.filter((d)=>d.family===o)??[],u=a.find((d)=>ee(i,ye(d),d.prefix))?.address??a.find((d)=>!xe(d.address))?.address??a[0]?.address,p=Ie(e,t);return{...n,kind:p?"container":r.gateway?"gateway":"network",iface:r.iface,gateway:r.gateway,route:r,source:u,container:p?.container}}function ht(e,t,o=[]){let n=(c)=>N(c)||L(e,c)?.kind==="loopback";if(t.every(n))return{scope:"local",interfaces:[te(e)]};let s=e.interfaces.filter((c)=>c.state!=="down"&&c.addresses.length);if(t.some(S)){let c=new Set(t.includes("0.0.0.0")?[4]:[]);if(t.includes("::")){c.add(6);let i=o.filter((a)=>a.local.address==="::");if(!i.length||i.some((a)=>!a.v6only))c.add(4)}return{scope:"everywhere",...!c.has(4)&&{ipv6Only:!0},interfaces:s.filter((i)=>!i.master).filter((i)=>i.addresses.some((a)=>c.has(a.family))).map((i)=>i.name)}}let r=new Set;for(let c of t){if(N(c))r.add(te(e));let i=L(e,c);if(i)r.add(i.name)}return{scope:"interfaces",interfaces:[...r]}}var gt=(e,t,o,n)=>e.proto===t&&e.hostPort===o&&(n===void 0||S(e.hostIp)||S(n)||e.hostIp===n);function bt(e,t,o,n){for(let s of e.containers)for(let r of s.ports)if(gt(r,t,o,n))return{container:s,published:r};return}var ve=(e)=>e.state==="LISTEN"||e.state==="UNCONN"&&e.local.port!==0;function R(e){let t=new Map;for(let n of e.sockets.filter(ve)){let s=bt(e,n.proto,n.local.port,n.local.address),r=s?`c${s.container.id}`:n.pid!==void 0?`p${n.pid}`:`u${n.uid??"?"}`,c=`${n.proto}/${n.local.port}/${r}`,i=t.get(c);if(!i)i={proto:n.proto,port:n.local.port,addresses:[],reach:{scope:"local",interfaces:[]},process:oe(e,n.pid),user:ne(e,n.uid),container:s?.container,published:s?.published,sockets:[],clients:[]},t.set(c,i);if(i.sockets.push(n),!i.addresses.includes(n.local.address))i.addresses.push(n.local.address)}for(let n of e.containers)for(let s of n.ports){if([...t.values()].some((a)=>a.proto===s.proto&&a.port===s.hostPort&&a.container?.id===n.id))continue;let c=`${s.proto}/${s.hostPort}/c${n.id}`,i=t.get(c);if(i){if(!i.addresses.includes(s.hostIp))i.addresses.push(s.hostIp);continue}t.set(c,{proto:s.proto,port:s.hostPort,addresses:[s.hostIp],reach:{scope:"local",interfaces:[]},user:"root",container:n,published:s,sockets:[],clients:[]})}let o=[...t.values()];for(let n of o)n.reach=ht(e,n.addresses,n.sockets);for(let n of E(e,o))if(n.direction==="in")C(o,n.proto,n.local)?.clients.push(n);return o.sort((n,s)=>n.port-s.port||n.proto.localeCompare(s.proto))}function C(e,t,o){return e.find((n)=>n.proto===t&&n.port===o.port&&n.addresses.some((s)=>s===o.address||S(s)))}function E(e,t){let o=t??R(e),n=[];for(let s of e.sockets){if(ve(s)||s.remote.port===0)continue;let r=C(o,s.proto,s.local)?"in":"out";n.push({proto:s.proto,local:s.local,remote:s.remote,state:s.state,process:oe(e,s.pid),user:s.orphan?void 0:ne(e,s.uid),direction:r,peer:kt(e,o,s)})}return n}function kt(e,t,o){let n=o.remote.address;if(N(n)||L(e,n)){let r=e.sockets.find((i)=>i!==o&&i.proto===o.proto&&i.local.port===o.remote.port&&i.remote.port===o.local.port&&i.local.address===n&&i.remote.address===o.local.address),c=C(t,o.proto,o.remote);return{kind:"local",process:oe(e,r?.pid),user:r?ne(e,r.uid):void 0,listener:c&&C(t,o.proto,o.local)!==c?c:void 0}}let s=Ie(e,n);if(s)return{kind:"container",container:s.container,network:s.network.name};return{kind:"remote",route:A(e,n)}}function Pe(e,t){return{port:t,listeners:R(e).filter((o)=>o.port===t),outbound:E(e).filter((o)=>o.direction==="out"&&o.remote.port===t&&!(o.peer.kind==="local"&&o.peer.listener?.port===t))}}function re(e){let t=R(e),o=E(e,t).filter((r)=>r.state==="ESTABLISHED"),n=[],s=[];for(let r of e.interfaces){if(r.master)continue;let c=e.containers.flatMap((u)=>u.networks.filter((p)=>p.bridge===r.name).map((p)=>({container:u,ip:p.ip}))),i={iface:r,routes:e.routes.filter((u)=>u.iface===r.name),listeners:t.filter((u)=>u.reach.scope!=="everywhere"&&u.reach.interfaces.includes(r.name)),outbound:o.filter((u)=>u.direction==="out"&&u.peer.kind==="remote"&&u.peer.route.iface===r.name),local:r.kind==="loopback"?o.filter((u)=>u.peer.kind==="local"&&u.direction==="out"):[],containers:c},a=!r.addresses.length&&!i.listeners.length&&!i.outbound.length&&!c.length;if(r.state==="down"&&a)s.push(r.name);else n.push(i)}return{hostname:e.hostname,environment:e.environment,everywhere:t.filter((r)=>r.reach.scope==="everywhere"),interfaces:n,idle:s,dns:e.dns.map((r)=>({server:r,route:A(e,r)})),hidden:e.hidden}}var Ne=(e)=>e.sockets.filter((t)=>t.pid===void 0&&!t.orphan&&t.uid!==e.uid).length;import{hostname as en}from"node:os";import{basename as wt}from"node:path";var $t={LISTEN:"LISTEN",ESTABLISHED:"ESTABLISHED",SYN_SENT:"SYN_SENT",SYN_RCVD:"SYN_RECV",FIN_WAIT_1:"FIN_WAIT1",FIN_WAIT_2:"FIN_WAIT2",TIME_WAIT:"TIME_WAIT",CLOSE_WAIT:"CLOSE_WAIT",LAST_ACK:"LAST_ACK",CLOSING:"CLOSING",CLOSED:"CLOSE"};function Te(e,t){let o=e.lastIndexOf("."),n=e.slice(0,o),s=e.slice(o+1);return{address:n==="*"?t===4?"0.0.0.0":"::":b(n).address,port:s==="*"?0:Number(s)}}function yt(e){let t=[];for(let o of e.split(`
|
|
3
|
+
`)){let n=o.trim().split(/\s+/),s=n[0]?.match(/^(tcp|udp)(4|6|46)$/);if(!s||n.length<5)continue;let r=s[1],c=s[2]==="46",i=s[2]==="4"?4:6,a=Te(n[3]??"",i),u=Te(n[4]??"",i),p=r==="udp"?u.port===0?"UNCONN":"ESTABLISHED":$t[n[5]??""];if(!p)continue;let d=b(a.address);t.push({proto:r,family:d.family,local:{address:d.address,port:a.port},remote:u,state:p,...i===6&&!c&&a.address==="::"&&{v6only:!0}})}return t}function Re(e,t){let o=e.match(/^\[?(.*?)\]?:(\d+|\*)$/),n=o?.[1]??"*";return{address:n==="*"?t?"::":"0.0.0.0":b(n).address,port:o?.[2]==="*"?0:Number(o?.[2]??0)}}function xt(e){let t=[],o=0,n=-1,s={};for(let r of e.split(`
|
|
4
|
+
`)){let c=r[0],i=r.slice(1);if(c==="p")o=Number(i);else if(c==="u")n=Number(i);else if(c==="f")s={};else if(c==="t")s.type=i;else if(c==="P")s.proto=i.toLowerCase();else if(c==="n"&&(s.proto==="tcp"||s.proto==="udp")){let a=s.type==="IPv6",[u="",p]=i.split("->");t.push({pid:o,uid:n,proto:s.proto,local:Re(u,a),remote:p?Re(p,a):void 0})}}return t}var Ee=(e,t,o)=>`${e} ${t.address} ${t.port} ${o?.port?`${o.address} ${o.port}`:"*"}`;function St(e,t){let o=new Map;for(let n of t){let s=Ee(n.proto,n.local,n.remote);if(!o.has(s))o.set(s,n)}return e.map((n)=>{let s=o.get(Ee(n.proto,n.local,n.remote));if(s)return{...n,pid:s.pid,uid:s.uid};return n.state==="TIME_WAIT"?{...n,orphan:!0}:n})}function It(e,t){let o=new Map;for(let r of t.split(`
|
|
5
|
+
`)){let c=r.match(/^\s*(\d+)\s+(.*)$/);if(c?.[1]&&c[2])o.set(Number(c[1]),c[2].trim())}let n=[],s={};for(let r of e.split(`
|
|
6
|
+
`)){let c=r.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/);if(!c?.[1]||!c[2]||!c[3]||!c[4])continue;let i=Number(c[1]),a=Number(c[2]);s[a]=c[3];let u=wt(c[4].trim());n.push({pid:i,name:u,cmd:o.get(i)||u,uid:a})}return{processes:n,users:s}}var vt=(e)=>Number.parseInt(e,16).toString(2).replace(/0+$/,"").length;function Pt(e,t){if(/^lo\d/.test(e))return"loopback";if(t.has(e))return"wifi";if(/^bridge\d/.test(e))return"bridge";if(/^(utun|ipsec|ppp|tun|tap)\d/.test(e))return"tun";if(/^vlan\d/.test(e))return"vlan";if(/^bond\d/.test(e))return"bond";if(/^en\d/.test(e))return"ethernet";return"virtual"}function Nt(e,t){let o=new Set([...t.matchAll(/Hardware Port: Wi-Fi\s*\nDevice: (\S+)/g)].map((r)=>r[1]??"")),n=[],s;for(let r of e.split(`
|
|
7
|
+
`)){let c=r.match(/^(\S+): flags=\w+<([^>]*)> mtu (\d+)/);if(c?.[1]){s={name:c[1],index:0,kind:Pt(c[1],o),state:"unknown",mtu:Number(c[3]),addresses:[],members:[],rxBytes:0,txBytes:0,flags:(c[2]??"").split(",")},n.push(s);continue}if(!s)continue;let i=r.trim().split(/\s+/);if(i[0]==="ether")s.mac=i[1];else if(i[0]==="inet"&&i[1]&&i.includes("netmask"))s.addresses.push({family:4,address:i[1],prefix:vt(i[i.indexOf("netmask")+1]??"0")});else if(i[0]==="inet6"&&i[1]){let a=Number(i[i.indexOf("prefixlen")+1]??128);s.addresses.push({family:6,address:b(i[1]).address,prefix:a})}else if(i[0]==="status:")s.status=i[1];else if(i[0]==="member:"&&i[1])s.members.push(i[1])}for(let r of n){let c=r.flags.includes("UP")&&r.flags.includes("RUNNING");r.state=r.status?r.status==="active"&&c?"up":"down":c?"up":"down";for(let i of r.members){let a=n.find((u)=>u.name===i);if(a)a.master=r.name}}return n.map(({status:r,flags:c,...i})=>i)}function Tt(e){let t=new Map;for(let o of e.split(`
|
|
8
|
+
`)){let n=o.trim().split(/\s+/),s=n[2]?.match(/^<Link#(\d+)>$/);if(!s?.[1]||!n[0]||t.has(n[0]))continue;let r=(c)=>Number(n[n.length-c])||0;t.set(n[0].replace(/\*$/,""),{index:Number(s[1]),rx:r(5),tx:r(2)})}return t}function Rt(e,t,o){if(e==="default")return{dest:t===4?"0.0.0.0":"::",prefix:0};let[n="",s]=e.split("/");if(t===6)return{dest:b(n).address,prefix:s?Number(s):128};let r=n.split("."),c=[...r,"0","0","0"].slice(0,4).join("."),i=s?Number(s):o?32:r.length*8;return{dest:c,prefix:i}}function Et(e){let t=[],o,n={};for(let s of e.split(`
|
|
9
|
+
`)){if(/^Internet:/.test(s))o=4;else if(/^Internet6:/.test(s))o=6;let r=s.trim().split(/\s+/);if(r[0]==="Destination"){n=Object.fromEntries(r.map((m,g)=>[m,g]));continue}if(!o||n.Netif===void 0||r.length<=n.Netif)continue;let[c="",i="",a=""]=[r[0],r[1],r[n.Flags??2]],u=r[n.Netif]??"";if(/[ILRB]/.test(a)||/^(22[4-9]|23\d|255)\b|^ff/.test(c))continue;let{dest:p,prefix:d}=Rt(c,o,a.includes("H")),l=a.includes("G")?b(i).address:void 0;t.push({family:o,dest:p,prefix:d,gateway:l,iface:u,metric:0})}return t}function Oe(e){let t=Nt(e("ifconfig",["-a"]),e("networksetup",["-listallhardwareports"])),o=Tt(e("netstat",["-ibn"]));t.forEach((c,i)=>{let a=o.get(c.name);c.index=a?.index??i+1,c.rxBytes=a?.rx??0,c.txBytes=a?.tx??0});let n=St(yt(e("netstat",["-an"])),xt(e("lsof",["-nP","-iTCP","-iUDP","-FpuftPn"]))),{processes:s,users:r}=It(e("ps",["-axo","pid=,uid=,user=,comm="]),e("ps",["-axww","-o","pid=,args="]));return{environment:"macos",interfaces:t,routes:Et(e("netstat",["-rn"])),sockets:n,processes:s,users:r,dns:[...e("cat",["/etc/resolv.conf"]).matchAll(/^\s*nameserver\s+(\S+)/gm)].map((c)=>b(c[1]??"").address),hidden:0}}import{spawnSync as Ot}from"node:child_process";function Lt(e){let t;try{t=JSON.parse(e)}catch{return[]}return t.map((o)=>{let n=[];for(let[s,r]of Object.entries(o.NetworkSettings?.Ports??{})){let[c="0",i="tcp"]=s.split("/");for(let a of r??[])n.push({proto:i,hostIp:a.HostIp||"0.0.0.0",hostPort:Number(a.HostPort),containerPort:Number(c)})}return{id:(o.Id??"").slice(0,12),name:(o.Name??"").replace(/^\//,""),image:o.Config?.Image??"",project:o.Config?.Labels?.["com.docker.compose.project"],networks:Object.entries(o.NetworkSettings?.Networks??{}).map(([s,r])=>({name:s,bridge:s==="bridge"?"docker0":r.NetworkID&&!["host","none"].includes(s)?`br-${r.NetworkID.slice(0,12)}`:void 0,ip:r.IPAddress||r.GlobalIPv6Address||void 0})),ports:n}})}function Le(){let e=(n)=>Ot("docker",n,{encoding:"utf8",timeout:5000}),t=e(["ps","-q"]);if(t.status!==0||!t.stdout.trim())return[];let o=e(["inspect",...t.stdout.trim().split(/\s+/)]);return o.status===0?Lt(o.stdout):[]}import{readFileSync as Jt}from"node:fs";import{join as D}from"node:path";import{existsSync as j,readdirSync as se,readFileSync as Ct,readlinkSync as At}from"node:fs";import{networkInterfaces as jt}from"node:os";import{basename as Ce,join as x}from"node:path";function Ae(){let e={};for(let[t,o]of Object.entries(jt()))e[t]=(o??[]).map((n)=>({family:n.family==="IPv6"?6:4,address:n.address.replace(/%.*$/,""),prefix:Number(n.cidr?.split("/")[1]??(n.family==="IPv6"?128:32))}));return e}function je(e,t){let o=x(e,"class/net"),n=[];try{n=se(o)}catch{}let s=n.map((r)=>{let c=x(o,r),i=(m)=>{try{return Ct(x(c,m),"utf8").trim()}catch{return""}},a=(m)=>Number(i(m))||0,u;try{u=Ce(At(x(c,"master")))}catch{}let p=[];try{p=se(x(c,"brif"))}catch{p=se(c).filter((m)=>m.startsWith("lower_")).map((m)=>m.slice(6))}let d=i("operstate"),l=i("address");return{name:r,index:a("ifindex"),kind:_t(c,i),state:d==="up"?"up":d==="down"?"down":"unknown",mtu:a("mtu"),mac:l&&l!=="00:00:00:00:00:00"?l:void 0,addresses:t[r]??[],master:u,members:p.sort(),rxBytes:a("statistics/rx_bytes"),txBytes:a("statistics/tx_bytes")}});for(let r of s)if(r.kind==="loopback"&&r.state==="unknown"&&r.addresses.length)r.state="up";return s.sort((r,c)=>r.index-c.index)}function _t(e,t){let o=t("type");if(o==="772")return"loopback";let n=t("uevent").match(/^DEVTYPE=(.*)$/m)?.[1];if(n==="wlan"||j(x(e,"wireless")))return"wifi";if(n==="bridge"||j(x(e,"bridge")))return"bridge";if(n==="vlan")return"vlan";if(n==="bond"||j(x(e,"bonding")))return"bond";if(n==="wireguard"||o==="65534")return n==="wireguard"?"wireguard":"tun";if(Ce(e).startsWith("veth"))return"veth";if(j(x(e,"device")))return"ethernet";return"virtual"}import{readdirSync as _e,readFileSync as We,readlinkSync as Wt}from"node:fs";import{join as _}from"node:path";function Me(e){let t=[],o=new Map,n=0,s=[];try{s=_e(e).filter((r)=>/^\d+$/.test(r))}catch{}s.sort((r,c)=>Number(r)-Number(c));for(let r of s){let c=Number(r),i=_(e,r),a;try{a=_e(_(i,"fd"))}catch{n++;continue}let u=0;for(let p of a){let d;try{d=Wt(_(i,"fd",p))}catch{continue}let l=d.match(/^socket:\[(\d+)\]$/);if(!l)continue;u++;let m=Number(l[1]);if(!o.has(m))o.set(m,c)}if(u===0)continue;t.push(Mt(i,c))}return{processes:t,owners:o,hidden:n}}function Mt(e,t){let o=(u)=>{try{return We(_(e,u),"utf8")}catch{return""}},n=o("comm").trim(),s=o("cmdline").split("\x00").filter(Boolean),r=s[0]?.split("/").pop(),c=(n&&s.join(" ").includes(n)?n:r||n)||String(t),i=s.join(" ")||`[${c}]`,a=Number(o("status").match(/^Uid:\s+(\d+)/m)?.[1]??-1);return{pid:t,name:c,cmd:i,uid:a}}function Fe(e){let t={},o="";try{o=We(e,"utf8")}catch{}for(let n of o.split(`
|
|
10
|
+
`)){let[s,,r]=n.split(":");if(s&&r&&/^\d+$/.test(r))t[Number(r)]=s}return t}import{readFileSync as Ft}from"node:fs";import{join as Dt}from"node:path";var De=1,Be=2,Ge=512;function Bt(e){let t=[];for(let o of e.split(`
|
|
11
|
+
`).slice(1)){let[n,s,r,c,,,i,a]=o.trim().split(/\s+/);if(!n||!s||!r||!c||!a)continue;let u=Number.parseInt(c,16);if(!(u&De)||u&Ge)continue;t.push({family:4,dest:P(s),prefix:Se(a),gateway:u&Be?P(r):void 0,iface:n,metric:Number(i)||0})}return t}function Gt(e){let t=[];for(let o of e.split(`
|
|
12
|
+
`)){let n=o.trim().split(/\s+/);if(n.length<10)continue;let[s="",r="0",,,c="",i="0",,,a="0",u=""]=n,p=Number.parseInt(a,16);if(!(p&De)||p&Ge)continue;let d=Z(s),l=Number.parseInt(r,16);if(d.startsWith("ff")||l===128&&u!=="lo"||u==="lo"&&d!=="::1")continue;let m=Z(c);t.push({family:6,dest:d,prefix:l,gateway:p&Be&&m!=="::"?m:void 0,iface:u,metric:Number.parseInt(i,16)})}return t}function He(e){let t=(o)=>{try{return Ft(Dt(e,o),"utf8")}catch{return""}};return[...Bt(t("net/route")),...Gt(t("net/ipv6_route"))]}import{readFileSync as Ht}from"node:fs";import{join as W}from"node:path";var Ut={"01":"ESTABLISHED","02":"SYN_SENT","03":"SYN_RECV","04":"FIN_WAIT1","05":"FIN_WAIT2","06":"TIME_WAIT","07":"CLOSE","08":"CLOSE_WAIT","09":"LAST_ACK","0A":"LISTEN","0B":"CLOSING"};function M(e,t,o){let n=[];for(let s of e.split(`
|
|
13
|
+
`).slice(1)){let r=s.trim().split(/\s+/);if(r.length<10)continue;let[,c="",i="",a="",,,,u="0",,p="0"]=r,d=(w)=>{let[f="",h="0"]=w.split(":");return{...o?$e(f):{family:4,address:P(f)},port:Number.parseInt(h,16)}},l=d(c),m=d(i),g=t==="udp"?m.port===0?"UNCONN":"ESTABLISHED":Ut[a.toUpperCase()];if(!g)continue;n.push({proto:t,family:l.family,local:{address:l.address,port:l.port},remote:{address:m.address,port:m.port},state:g,uid:Number(u),inode:Number(p),...p==="0"&&{orphan:!0}})}return n}var F=(e)=>{try{return Ht(e,"utf8")}catch{return""}};function Ue(e){return[...M(F(W(e,"net/tcp")),"tcp",!1),...M(F(W(e,"net/tcp6")),"tcp",!0),...M(F(W(e,"net/udp")),"udp",!1),...M(F(W(e,"net/udp6")),"udp",!0)]}function Je(e,t){let o=D(e,"proc"),n=(i)=>{try{return Jt(D(e,i),"utf8")}catch{return""}},s=Me(o),r=n("proc/sys/net/ipv6/bindv6only").trim()==="1",c=Ue(o).map((i)=>({...i,pid:i.inode===void 0?void 0:s.owners.get(i.inode),...r&&i.local.address==="::"&&{v6only:!0}}));return{environment:/microsoft|wsl/i.test(n("proc/sys/kernel/osrelease"))?"wsl":"linux",interfaces:je(D(e,"sys"),t??Ae()),routes:He(o),sockets:c,processes:s.processes,users:Fe(D(e,"etc/passwd")),dns:[...n("etc/resolv.conf").matchAll(/^\s*nameserver\s+(\S+)/gm)].map((i)=>i[1]??""),hidden:s.hidden}}import{spawnSync as Kt}from"node:child_process";var Ke=(e,t)=>{let o=Kt(e,t,{encoding:"utf8",timeout:15000,maxBuffer:67108864,windowsHide:!0});return o.error?"":o.stdout??""};var Vt=`
|
|
14
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
15
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
16
|
+
[Console]::OutputEncoding = [Text.Encoding]::UTF8
|
|
17
|
+
$ifaces = foreach ($n in [System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces()) {
|
|
18
|
+
$p = $n.GetIPProperties()
|
|
19
|
+
$v4 = $null; try { $v4 = $p.GetIPv4Properties() } catch {}
|
|
20
|
+
$v6 = $null; try { $v6 = $p.GetIPv6Properties() } catch {}
|
|
21
|
+
$st = $null; try { $st = $n.GetIPStatistics() } catch {}
|
|
22
|
+
[ordered]@{
|
|
23
|
+
name = $n.Name; description = $n.Description; type = "$($n.NetworkInterfaceType)"
|
|
24
|
+
status = "$($n.OperationalStatus)"; mac = "$($n.GetPhysicalAddress())"
|
|
25
|
+
index = if ($v4) { $v4.Index } elseif ($v6) { $v6.Index } else { 0 }
|
|
26
|
+
mtu = if ($v4) { $v4.Mtu } elseif ($v6) { $v6.Mtu } else { 0 }
|
|
27
|
+
rx = if ($st) { $st.BytesReceived } else { 0 }; tx = if ($st) { $st.BytesSent } else { 0 }
|
|
28
|
+
addresses = @($p.UnicastAddresses | ForEach-Object { ,@("$($_.Address)", $_.PrefixLength) })
|
|
29
|
+
dns = @($p.DnsAddresses | ForEach-Object { "$_" })
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
$g = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()
|
|
33
|
+
[ordered]@{
|
|
34
|
+
tcp4 = @($g.GetActiveTcpListeners() | Where-Object { "$($_.Address)" -eq '0.0.0.0' } | ForEach-Object { $_.Port })
|
|
35
|
+
udp4 = @($g.GetActiveUdpListeners() | Where-Object { "$($_.Address)" -eq '0.0.0.0' } | ForEach-Object { $_.Port })
|
|
36
|
+
tcp = @(Get-NetTCPConnection | ForEach-Object { ,@($_.LocalAddress, $_.LocalPort, $_.RemoteAddress, $_.RemotePort, "$($_.State)", $_.OwningProcess) })
|
|
37
|
+
udp = @(Get-NetUDPEndpoint | ForEach-Object { ,@($_.LocalAddress, $_.LocalPort, $_.OwningProcess) })
|
|
38
|
+
interfaces = @($ifaces)
|
|
39
|
+
routes = @(Get-NetRoute | ForEach-Object { ,@($_.DestinationPrefix, $_.NextHop, $_.InterfaceAlias, ($_.RouteMetric + $_.InterfaceMetric)) })
|
|
40
|
+
processes = @(Get-CimInstance Win32_Process -Property ProcessId, Name, CommandLine | ForEach-Object { ,@($_.ProcessId, $_.Name, $_.CommandLine) })
|
|
41
|
+
} | ConvertTo-Json -Depth 5 -Compress
|
|
42
|
+
`,Yt={Listen:"LISTEN",Established:"ESTABLISHED",SynSent:"SYN_SENT",SynReceived:"SYN_RECV",FinWait1:"FIN_WAIT1",FinWait2:"FIN_WAIT2",TimeWait:"TIME_WAIT",CloseWait:"CLOSE_WAIT",LastAck:"LAST_ACK",Closing:"CLOSING",Bound:"CLOSE",Closed:"CLOSE",DeleteTCB:"CLOSE"};function qt(e,t){if(e==="Loopback")return"loopback";if(/wireguard/i.test(t))return"wireguard";if(/tap-windows|wintun|openvpn|tailscale|vpn/i.test(t)||/Tunnel|Ppp/.test(e))return"tun";if(/virtual|hyper-v|vmware|bluetooth|wi-fi direct/i.test(t))return"virtual";if(e==="Wireless80211")return"wifi";if(/Ethernet/.test(e))return"ethernet";return"virtual"}var zt=(e)=>e.length===12?e.toLowerCase().replace(/(..)(?!$)/g,"$1:"):void 0,Xt=(e)=>e.startsWith("fec0:0:0:ffff::"),Qt=(e)=>/^(22[4-9]|23\d)\./.test(e)||e.startsWith("ff");function Zt(e){let t=(e.interfaces??[]).map((d)=>{let l=d.addresses.map(([m,g])=>({...b(m),prefix:g}));return{name:d.name,index:d.index,kind:qt(d.type,d.description),state:d.status==="Up"?"up":d.status==="Down"?"down":"unknown",mtu:d.mtu>0?d.mtu:0,mac:zt(d.mac),addresses:l,members:[],rxBytes:d.rx,txBytes:d.tx}}),o=new Set(t.flatMap((d)=>d.addresses.map((l)=>l.address))),n=[];for(let[d,l,m,g]of e.routes??[]){let[w="",f="0"]=d.split("/"),h=b(w),k=Number(f),ft=k===(h.family===4?32:128);if(Qt(h.address)||h.address==="255.255.255.255")continue;if(ft&&(o.has(h.address)||h.address.endsWith(".255")))continue;let q=b(l).address;n.push({family:h.family,dest:h.address,prefix:k,gateway:q==="0.0.0.0"||q==="::"?void 0:q,iface:m,metric:g})}let s=(d,l,m,g,w,f,h)=>{let k=b(l);return{proto:d,family:k.family,local:{address:k.address,port:m},remote:{address:b(g).address,port:w},state:f,...h===0?{orphan:!0}:{pid:h},...k.address==="::"&&!i(d,m)&&{v6only:!0}}},r=new Set([...(e.tcp??[]).filter((d)=>d[0]==="0.0.0.0").map((d)=>`tcp ${d[1]}`),...(e.udp??[]).filter((d)=>d[0]==="0.0.0.0").map((d)=>`udp ${d[1]}`)]),c=new Set([...(e.tcp4??[]).map((d)=>`tcp ${d}`),...(e.udp4??[]).map((d)=>`udp ${d}`)]),i=(d,l)=>c.has(`${d} ${l}`)&&!r.has(`${d} ${l}`),a=[];for(let[d,l,m,g,w,f]of e.tcp??[]){let h=Yt[w];if(h)a.push(s("tcp",d,l,m,g,h,f))}for(let[d,l,m]of e.udp??[])a.push(s("udp",d,l,d.includes(":")?"::":"0.0.0.0",0,"UNCONN",m));let u=(e.processes??[]).map(([d,l,m])=>{let g=l.replace(/\.exe$/i,"");return{pid:d,name:g,cmd:m?.trim()||g}}),p=[...new Set((e.interfaces??[]).filter((d)=>d.status==="Up"&&d.type!=="Loopback").flatMap((d)=>d.dns.map((l)=>b(l).address)).filter((d)=>!Xt(d)))];return{environment:"windows",interfaces:t,routes:n,sockets:a,processes:u,users:{},dns:p,hidden:0}}function Ve(e){let t=Buffer.from(Vt,"utf16le").toString("base64"),o=e("powershell.exe",["-NoProfile","-NonInteractive","-EncodedCommand",t]),n={};try{n=JSON.parse(o.replace(/^/,""))}catch{}return Zt(n)}function Ye(e={}){let t=e.platform??process.platform,o=e.run??Ke,n=t==="win32"?Ve(o):t==="darwin"?Oe(o):Je(e.root??"/",e.addresses),s=new Set(n.interfaces.map((c)=>c.name)),r=(e.containers??Le()).map((c)=>({...c,networks:c.networks.map((i)=>({...i,bridge:i.bridge&&s.has(i.bridge)?i.bridge:void 0}))}));return{hostname:e.hostname??en(),...n,containers:r,uid:e.uid??process.getuid?.()}}function y(e){let t=(o)=>(n)=>e.color&&n?`\x1B[${o}m${n}\x1B[0m`:n;return{bold:t("1"),dim:t("2"),red:t("31"),green:t("32"),yellow:t("33"),blue:t("34"),magenta:t("35"),cyan:t("36")}}function ie(e,t){return t!==void 0&&t>0&&e.length>t?`${e.slice(0,Math.max(0,t-1))}…`:e}function qe(e,t,o){let n=e[0]?.map((s,r)=>Math.max(...e.map((c)=>(c[r]??"").length)))??[];return e.map((s)=>{let r=0;return s.map((c,i)=>{let u=i===s.length-1?ie(c,o===void 0?void 0:Math.max(10,o-r)):c.padEnd(n[i]??0);return r+=(n[i]??0)+2,(t[i]??((p)=>p))(u)}).join(" ").trimEnd()}).join(`
|
|
43
|
+
`)}var ce=(e)=>`${e.name} ${e.pid}`,H=(e)=>e?.windows?"run as administrator":"run with sudo",ae=(e)=>e.process?ce(e.process):e.user?`${e.user}'s process`:"closing, no process";function U(e,t){if(e.container)return`container ${e.container.name}${e.published?` :${e.published.containerPort}`:""}`;if(e.process)return ce(e.process);let o=e.sockets.find((n)=>n.pid!==void 0)?.pid;if(o!==void 0)return`pid ${o}`;return`${e.user??"another user"}'s process (${t?.windows?"run as administrator":"sudo"} to see it)`}function Qe(e,t){let o=t.wsl&&e.addresses.some((s)=>S(s)||s.startsWith("127.")),n=e.reach.scope==="local"?"this machine only":e.reach.scope==="everywhere"?e.reach.ipv6Only?"every interface, IPv6 only":"every interface":e.reach.interfaces.join(", ")||"no interface";return o?`${n} + Windows`:n}var tn=(e)=>{let t=e.addresses;if(t.length===2&&t.includes("0.0.0.0")&&t.includes("::"))return"*";return t.length>2?`${t.slice(0,2).join(", ")} +${t.length-2}`:t.join(", ")};function B(e,t,o){switch(e.kind){case"local":{let n=e.listener?U(e.listener,o):e.process?ce(e.process):e.user&&`${e.user}'s process`||"this machine";return`${t} ${n}`}case"container":return`${t} container ${e.container.name}`;case"remote":return t}}var de=(e)=>e.kind==="unreachable"?"no route":e.gateway?`via ${e.iface} → ${e.gateway}`:`via ${e.iface}`;function Ze(e,t){let o=y(t);if(!e.length)return o.dim("nothing is listening");let n=[["PORT","ADDRESS","REACHABLE FROM","CLIENTS","OWNER"],...e.map((a)=>[`${a.proto} ${a.port}`,tn(a),Qe(a,t),a.clients.length?String(a.clients.length):"",U(a,t)])],s=(a)=>a.startsWith("this machine")?o.green(a):a.startsWith("every")?o.yellow(a):a,r=[o.bold,(a)=>a,s,o.cyan,(a)=>a],[c=""]=qe(n,r.map(()=>(a)=>a),t.width).split(`
|
|
44
|
+
`),i=qe(n,r,t.width).split(`
|
|
45
|
+
`).slice(1);return[o.dim(c),...i].join(`
|
|
46
|
+
`)}var ze=(e)=>{let t=["B","kB","MB","GB","TB"],o=0,n=e;while(n>=1000&&o<t.length-1)n/=1000,o++;return`${o?n.toFixed(1):n} ${t[o]}`},nn=(e)=>e.addresses.map((t)=>`${t.address}/${t.prefix}`);function et(e,t,o=""){let n=y(t),s=e.state==="up"?n.green("up"):e.state==="down"?n.red("down"):n.dim(e.state);return[n.bold(e.name),e.kind,s,nn(e).join(" "),o&&n.dim(o)].filter(Boolean).join(" ")}var tt=(e)=>{let t=[...new Set(e.containers.flatMap((o)=>o.container.networks.filter((n)=>n.bridge===e.iface.name).map((n)=>n.name)))];return t.length?`docker network ${t.join(", ")}`:""},G=(e)=>e.filter((t)=>!(t.family===6&&t.dest==="fe80::")),ue=(e)=>(e.prefix===0?"default":`${e.dest}/${e.prefix}`)+(e.gateway?` via ${e.gateway}`:"");function nt(e,t,o){let n=y(o),s=[];for(let r of e){let c=r.iface;s.push(et(c,o,tt(r)));let i=[];if(G(r.routes).length)i.push(["routes",G(r.routes).map(ue).join(", ")]);if(c.members.length){let p=c.members.filter((d)=>d.startsWith("veth")).length;i.push(["ports",p===c.members.length?`${p} veth`:c.members.join(", ")])}if(r.containers.length)i.push(["containers",r.containers.map((p)=>`${p.container.name}${p.ip?` ${p.ip}`:""}`).join(", ")]);let a=r.listeners.length;if(a)i.push(["listening",`${a} port${a>1?"s":""} bound here`]);if(r.outbound.length)i.push(["out",`${r.outbound.length} connection${r.outbound.length>1?"s":""}`]);let u=[c.mac,c.mtu?`mtu ${c.mtu}`:"",`↓ ${ze(c.rxBytes)} ↑ ${ze(c.txBytes)}`].filter(Boolean);i.push(["",u.join(" ")]);for(let[p,d]of i)s.push(` ${n.dim(p.padEnd(10))} ${ie(d,o.width&&o.width-13)}`);s.push("")}if(t.length)s.push(n.dim(`down, no address: ${t.join(", ")}`));return s.join(`
|
|
47
|
+
`).trimEnd()}function ot(e,t){let o=y(t);if(!e.length)return o.dim("no connections");let n=new Map;for(let r of e){let c=ae(r);n.set(c,[...n.get(c)??[],r])}let s=[];for(let[r,c]of[...n].sort((i,a)=>a[1].length-i[1].length)){s.push(`${o.bold(r)} ${o.dim(`${c.length} connection${c.length>1?"s":""}`)}`);let i=new Map;for(let a of c){let u=a.direction==="in"?o.magenta("←"):o.cyan("→"),p=a.direction==="in"?`:${a.local.port} `:"",d=a.peer.kind==="remote"?o.dim(de(a.peer.route)):"",l=a.state==="ESTABLISHED"?"":o.yellow(a.state),m=[` ${p}${u} ${B(a.peer,T(a.remote),t)}`,d,l].filter(Boolean).join(" ");i.set(m,(i.get(m)??0)+1)}for(let[a,u]of i)s.push(u>1?`${a} ${o.dim(`×${u}`)}`:a)}return s.join(`
|
|
48
|
+
`)}function rt(e,t){let o=y(t),n=[];if(!e.listeners.length&&!e.outbound.length)return`nothing listens on port ${e.port}, and nothing here talks to it`;for(let s of e.listeners){n.push(`${o.bold(`${s.proto} ${s.port}`)} ${U(s,t)}`);let r=[];if(s.process)r.push(["process",`${s.process.cmd} ${o.dim(`(pid ${s.process.pid}${s.user?`, ${s.user}`:""})`)}`]);else if(!s.container)r.push(["process",o.yellow(`owned by ${s.user??"another user"}: ${H(t)} to see which process`)]);if(s.container){let c=s.container;r.push(["container",`${c.name} ${o.dim(c.image)}${c.project?o.dim(` compose project ${c.project}`):""}`]);for(let i of c.networks)r.push(["",`→ ${i.ip??"?"}:${s.published?.containerPort??s.port} on ${i.name}${i.bridge?o.dim(` (${i.bridge})`):""}`])}if(r.push(["bound to",s.addresses.join(", ")]),r.push(["reachable",Qe(s,t)+(s.reach.scope==="everywhere"?o.dim(`: ${s.reach.interfaces.join(", ")}`):"")]),s.reach.scope==="everywhere")r.push(["",o.yellow("any machine that can reach one of those interfaces can connect")]);if(s.clients.length){r.push(["clients",`${s.clients.length}`]);for(let c of s.clients)r.push(["",`← ${B(c.peer,T(c.remote),t)}`])}else r.push(["clients",o.dim("none right now")]);for(let[c,i]of r)n.push(` ${o.dim(c.padEnd(10))} ${i}`);n.push("")}if(e.outbound.length){n.push(o.bold(`connections to port ${e.port} elsewhere`));for(let s of e.outbound){let r=ae(s),c=s.peer.kind==="remote"?` ${o.dim(de(s.peer.route))}`:"";n.push(` ${r} → ${B(s.peer,T(s.remote),t)}${c}`)}}return n.join(`
|
|
49
|
+
`).trimEnd()}function st(e,t,o){let n=y(o),s=t===e.dest?e.dest:`${t} (${e.dest})`,r=[];switch(e.kind){case"loopback":return`${n.bold(s)} is this machine, through the loopback ${n.dim("(lo)")}`;case"local":return`${n.bold(s)} is this machine: an address of ${e.iface}, delivered through the loopback`;case"unreachable":return`${n.bold(s)}: ${n.red("no route")}: packets to it are dropped`;case"container":r.push(["is",`container ${e.container?.name}, on this machine`]);break;case"network":r.push(["is",`on the network of ${e.iface}, reached directly`]);break;case"gateway":r.push(["is",`beyond ${e.gateway}, the gateway on ${e.iface}`])}if(r.push(["interface",e.iface??""]),e.route)r.push(["route",`${ue(e.route)}${e.route.metric?` metric ${e.route.metric}`:""}`]);if(e.source)r.push(["from",e.source]);return[n.bold(s),...r.map(([c,i])=>` ${n.dim(c.padEnd(10))} ${i}`)].join(`
|
|
50
|
+
`)}function it(e,t){let o=y(t),n=[];n.push(`${o.bold(e.hostname)}${e.environment==="wsl"?o.dim(" (WSL: Windows reaches ports on 127.0.0.1 and every interface through its localhost)"):""}`),n.push("");let s=(r)=>`${o.bold(`${r.proto} ${r.port}`.padEnd(9))} ${U(r,t)}${r.clients.length?o.cyan(` ← ${r.clients.length} client${r.clients.length>1?"s":""}`):""}`;if(e.everywhere.length){n.push(`${o.yellow("listening on every interface")}`);for(let r of e.everywhere)n.push(` ${s(r)}`);n.push("")}for(let r of e.interfaces){let c=r.iface,i=tt(r);if(n.push(et(c,t,i)),G(r.routes).length)n.push(` ${o.dim("routes")} ${G(r.routes).map(ue).join(", ")}`);for(let a of r.listeners)n.push(` ${o.green("listen")} ${s(a)}`);for(let a of r.containers)n.push(` ${o.blue("holds")} ${a.container.name}${a.ip?` ${a.ip}`:""}${on(a.container)}`);if(c.members.length)n.push(o.dim(` ${c.members.length} port${c.members.length>1?"s":""}: ${c.members.join(" ")}`).slice(0,t.width??1/0));n.push(...Xe(r.outbound,"out",t)),n.push(...Xe(r.local,"local",t)),n.push("")}if(e.idle.length)n.push(o.dim(`down, no address: ${e.idle.join(", ")}`));if(e.dns.length)n.push(`${o.dim("dns")} ${e.dns.map((r)=>`${r.server} ${o.dim(r.route.kind==="loopback"||r.route.kind==="local"?"(local resolver)":de(r.route))}`).join(", ")}`);if(e.hidden)n.push(o.dim(`${e.hidden} processes of other users hidden: ${H(t)} to see their sockets`));return n.join(`
|
|
51
|
+
`).trimEnd()}function on(e){let t=e.ports.filter((o,n,s)=>s.findIndex((r)=>r.hostPort===o.hostPort&&r.proto===o.proto)===n);return t.length?` ← host ${t.map((o)=>`:${o.hostPort}${o.hostPort!==o.containerPort?`→${o.containerPort}`:""}`).join(" ")}`:""}function Xe(e,t,o){let n=y(o),s=new Map;for(let c of e){let i=ae(c),a=t==="local"?B(c.peer,`:${c.remote.port}`,o):T(c.remote),u=s.get(i)??new Map;u.set(a,(u.get(a)??0)+1),s.set(i,u)}let r=t==="out"?n.cyan("out"):n.magenta("local");return[...s].map(([c,i])=>{let a=[...i].map(([u,p])=>p>1?`${u} ×${p}`:u).join(", ");return` ${r}${t==="out"?" ":" "}${ie(`${c} → ${a}`,o.width&&o.width-10)}`})}var pe=(e)=>e&&{pid:e.pid,name:e.name,cmd:e.cmd},J=(e)=>e&&{id:e.id,name:e.name,image:e.image,project:e.project};function rn(e){switch(e.kind){case"local":return{kind:e.kind,process:pe(e.process),user:e.user,listener:e.listener&&{proto:e.listener.proto,port:e.listener.port,container:e.listener.container?.name}};case"container":return{kind:e.kind,container:J(e.container),network:e.network};case"remote":return{kind:e.kind,route:K(e.route)}}}var K=(e)=>({...e,container:J(e.container)}),I=(e)=>({...e,process:pe(e.process),peer:rn(e.peer)}),V=(e)=>({proto:e.proto,port:e.port,addresses:e.addresses,reach:e.reach,process:pe(e.process),user:e.user,container:J(e.container),published:e.published,clients:e.clients.map(I)}),le=(e)=>({...e.iface,routes:e.routes,listeners:e.listeners.map((t)=>({proto:t.proto,port:t.port})),outbound:e.outbound.map(I),local:e.local.map(I),containers:e.containers.map((t)=>({...J(t.container),ip:t.ip}))}),ct=(e)=>({...e,everywhere:e.everywhere.map(V),interfaces:e.interfaces.map(le),dns:e.dns.map((t)=>({server:t.server,route:K(t.route)}))});var sn={json:["--json","machine-readable output"],tcp:["--tcp","TCP only"],udp:["--udp","UDP only"],all:["-a, --all","also connections that are opening or closing (TIME_WAIT…)"],"no-docker":["--no-docker","don't ask docker which containers publish ports"],"show-secrets":["--show-secrets",`don't mask the values of --token=, --password and the like
|
|
52
|
+
in command lines`],help:["-h, --help","show help: netbeagle COMMAND --help for one command"],version:["-v, --version","show the version"]},Y={list:{usage:["[list]"],summary:"every listening port: its process or container, and who can reach it",about:`Lists every port something listens on, with the process (or the docker container)
|
|
17
53
|
behind it, the addresses it is bound to, and who can reach it: this machine only,
|
|
18
54
|
every interface, or the interfaces holding its address. Ports docker publishes
|
|
19
55
|
without a socket of their own are listed too. This is what netbeagle does with no
|
|
@@ -31,20 +67,20 @@ kernel's main routing table decides: this machine, a container, a network one of
|
|
|
31
67
|
the interfaces is on, or a gateway; with the route and the source address.`,options:["json","no-docker"],examples:["netbeagle route github.com","netbeagle route 172.18.0.2"]},map:{usage:["map"],summary:"how everything connects, interface by interface",about:`The whole picture: the ports open on every interface; then each interface with
|
|
32
68
|
its routes, the ports bound to it, the containers behind it, and the connections
|
|
33
69
|
leaving through it; on the loopback, which local process talks to which. Ends
|
|
34
|
-
with the DNS servers and the way to them.`,options:["json","no-docker"],examples:["netbeagle map"]}},
|
|
70
|
+
with the DNS servers and the way to them.`,options:["json","no-docker"],examples:["netbeagle map"]}},fe={ls:"list",if:"interfaces",ifaces:"interfaces",connections:"conns",graph:"map"},at=(e)=>{let t=[...e,"help"].map((n)=>sn[n]).filter((n)=>!!n),o=Math.max(...t.map(([n])=>n.length));return t.map(([n,s])=>` ${n.padEnd(o)} ${s.replaceAll(`
|
|
35
71
|
`,`
|
|
36
|
-
${" ".repeat(
|
|
37
|
-
`)}function
|
|
38
|
-
`)}`,"",t.about,"","Options:",...
|
|
39
|
-
`)}var
|
|
72
|
+
${" ".repeat(o)} `)}`)};function me(){let e=Object.values(Y),t=Math.max(...e.map((o)=>(o.usage[0]??"").length));return["Usage: netbeagle [COMMAND] [OPTIONS]","","Commands:",...e.map((o)=>` ${(o.usage[0]??"").padEnd(t)} ${o.summary}`),"","Options:",...at(["json","no-docker","show-secrets","version"]),"","netbeagle COMMAND --help shows one command's usage, options and examples.",process.platform==="win32"?"Without an administrator terminal, other users' command lines are hidden.":"Without root, sockets of other users' processes show no process: sudo shows them all.","","From the same pack: envhound, which startup file sets each environment variable."," npx envhound",""].join(`
|
|
73
|
+
`)}function dt(e){let t=Y[fe[e]??e];if(!t)return;return[`Usage: ${t.usage.map((o)=>`netbeagle ${o}`).join(`
|
|
74
|
+
`)}`,"",t.about,"","Options:",...at(t.options),"","Examples:",...t.examples.map((o)=>` ${o}`),""].join(`
|
|
75
|
+
`)}var he=["...........tttttttttt...........",".........tttttttttttttt.........","........tttttttccttttttt........","......tttttttttccttttttttt......",".....ttttttttttcctttttttttt.....","...eetttttttttcccctttttttttee...","..eeetttttttttccccttttttttteee..","..eeetttbbbbttccccttbbbbttteee..",".eeeetttttttttccccttttttttteeee.",".eeeettttwddttccccttwddtttteeee.",".eeeettttdddttccccttdddtttteeee.",".eeeettttdddtcccccctdddtttteeee.",".eeeettttttttcccccctttttttteeee.",".eeeettttcccccddddccccctttteeee.",".eeeetttcccccddddddcccccttteeee.",".eeeetttcccccddddddcccccttteeee.",".eeeetttccccccddddccccccttteeee.",".eeeettttccdcccddcccdcctttteeee.","..eeeeettcccdddppdddccctteeeee..","..eeeeeettccccppppcccctteeeeee..","...eeeeeetttccppppccttteeeeee...","....eeTTTTTTttttttttTTTTTTee....",".ffffTTTTTTTffffffffTTTTTTTffff.",".fkkkTbTbTbTkkkkkkkkTbTbTbTkkkf.",".fkkkookkkookkkGGkkkookkkggkkkf.",".fkkkkkkkkkkkkkkkkkkkkkkkkkkkkf.",".fkkooookooooklyylkooooklyylkkf.",".fkkooookooookllllkooookllllkkf.",".fkkkookkkookkkllkkkookkkllkkkf.",".fffffffffffffffllfffffffffllff."],ge={t:["e08a3a",173],T:["e08a3a",173],e:["8c4214",94],b:["a8521a",130],c:["fbf1e3",255],d:["2b1d14",234],w:["ffffff",231],p:["f07c8a",210],f:["4a5266",240],k:["0f1218",233],o:["3a4152",238],l:["4fa3e0",74],y:["f3c969",222],g:["7fd18b",114],G:["7fd18b",114]};function ut(e,t){let o=([i,a],u)=>t.truecolor?`${u};2;${parseInt(i.slice(0,2),16)};${parseInt(i.slice(2,4),16)};${parseInt(i.slice(4),16)}`:`${u};5;${a}`,n=(i)=>i!=="."&&(t.color||!"dwk".includes(i)),s=(i,a)=>{if(!n(i)&&!n(a))return" ";if(!t.color)return n(i)?n(a)?"█":"▀":"▄";if(!n(i))return`\x1B[${o(ge[a],38)}m▄\x1B[0m`;let u="G".includes(i)?"5;":"",p=n(a)?`;${o(ge[a],48)}`:"";return`\x1B[${u}${o(ge[i],38)}${p}m▀\x1B[0m`},r=[];for(let i=0;i*2<he.length;i++){let a=he[i*2],u=he[i*2+1]??".".repeat(a.length),p="";for(let l=0;l<a.length;l++)p+=s(a[l],u[l]);let d=p.replace(/ +$/,"");r.push({out:d,width:a.length-(p.length-d.length)})}let c=Math.floor((r.length-e.length)/2);if((t.columns||80)<38+Math.max(...e.map((i)=>i.length)))return[...r.map((i)=>` ${i.out}`),"",...e.map((i)=>i?` ${i}`:"")].join(`
|
|
40
76
|
`)+`
|
|
41
|
-
`;return r.map((
|
|
77
|
+
`;return r.map((i,a)=>{let u=e[a-c];return u?` ${i.out}${" ".repeat(32-i.width)} ${u}`:` ${i.out}`}).join(`
|
|
42
78
|
`)+`
|
|
43
|
-
`}var
|
|
44
|
-
see: ${
|
|
45
|
-
${
|
|
46
|
-
${
|
|
79
|
+
`}var cn=new RegExp("((?:^|\\s)--?[\\w-]*(?:token|secret|passw(?:or)?d|api[-_]?key|private[-_]?key|credential|auth(?![a-z]))[\\w-]*(?:=|\\s+)|[\\w-]*(?:token|secret|passw(?:or)?d|api[-_]?key|private[-_]?key|credential|auth(?![a-z]))[\\w-]*=)(?!-)[^\\s&]+","gi"),pt=(e)=>e.replace(cn,"$1********");var pn="see what listens on each port, who reaches it, and how everything connects",lt="netbeagle --help";function v(e){return console.error(`netbeagle: ${e}
|
|
80
|
+
see: ${lt}`),2}async function ln(e){let{values:t,positionals:o}=un({args:e,allowPositionals:!0,options:{json:{type:"boolean"},tcp:{type:"boolean"},udp:{type:"boolean"},all:{type:"boolean",short:"a"},"no-docker":{type:"boolean"},"show-secrets":{type:"boolean"},help:{type:"boolean",short:"h"},version:{type:"boolean",short:"v"}}}),n=process.stdout.isTTY,s=()=>ut([`netbeagle ${O.version}`,"","see what listens on each","port, who reaches it, and","how everything connects"],{color:!process.env.NO_COLOR,truecolor:/^(truecolor|24bit)$/i.test(process.env.COLORTERM??""),columns:process.stdout.columns}),r=o[0]==="help",c=(r?o[1]:t.help?o[0]:void 0)??void 0;if(t.help||r){if(c){let f=dt(c);if(!f)return v(`no command '${c}'`);return process.stdout.write(f),0}return process.stdout.write(n?`${s()}
|
|
81
|
+
${me()}`:`netbeagle ${O.version}
|
|
82
|
+
${pn}
|
|
47
83
|
|
|
48
|
-
${
|
|
49
|
-
`),0;let[
|
|
50
|
-
${
|
|
84
|
+
${me()}`),0}if(t.version)return process.stdout.write(n?s():`${O.version}
|
|
85
|
+
`),0;let[i="list",...a]=o,u=fe[i]??i;if(!Y[u])return v(`no command '${i}'`);if(lt=`netbeagle ${u} --help`,!["linux","darwin","win32"].includes(process.platform))return console.error(`netbeagle runs on Linux, macOS and Windows, not ${process.platform}`),2;let p=process.stdout.isTTY,d=Ye(t["no-docker"]?{containers:[]}:{});if(!t["show-secrets"])for(let f of d.processes)f.cmd=pt(f.cmd);let l={color:p&&!process.env.NO_COLOR,width:p?process.stdout.columns:void 0,wsl:d.environment==="wsl",windows:d.environment==="windows"},m=(f,h)=>console.log(t.json?JSON.stringify(h(),null,2):f()),g=(f)=>t.tcp||t.udp?!!t[f]:!0,w=()=>{let f=Ne(d);if(f&&!t.json)console.error(y({...l,color:!!process.stderr.isTTY&&!process.env.NO_COLOR}).dim(`
|
|
86
|
+
${f} sockets belong to other users' processes: ${H(l)} to see which`))};switch(u){case"list":{let f=R(d).filter((h)=>g(h.proto));return m(()=>Ze(f,l),()=>f.map(V)),w(),0}case"who":{let f=Number(a[0]?.replace(/^:/,""));if(!a[0]||!Number.isInteger(f)||f<1||f>65535)return v("who takes a port number, e.g. netbeagle who 5432");let h=Pe(d,f);return m(()=>rt(h,l),()=>({port:f,listeners:h.listeners.map(V),outbound:h.outbound.map(I)})),h.listeners.length||h.outbound.length?0:1}case"conns":{let f=E(d).filter((h)=>g(h.proto)).filter((h)=>t.all||h.state==="ESTABLISHED");return m(()=>ot(f,l),()=>f.map(I)),w(),0}case"interfaces":{let f=re(d);return m(()=>nt(f.interfaces,f.idle,l),()=>f.interfaces.map(le)),0}case"route":{let f=a[0];if(!f)return v("route takes a host name or an address, e.g. netbeagle route github.com");let h=f;if(!dn(f))try{h=(await an(f)).address}catch{return console.error(`netbeagle: cannot resolve ${f}`),1}let k=A(d,h);return m(()=>st(k,f,l),()=>({host:f,...K(k)})),k.kind==="unreachable"?1:0}case"map":{let f=re(d);return m(()=>it(f,l),()=>ct(f)),0}}return v(`no command '${i}'`)}ln(process.argv.slice(2)).then((e)=>{process.exitCode=e},(e)=>{if(e instanceof Error&&"code"in e&&String(e.code).startsWith("ERR_PARSE_ARGS"))process.exitCode=v(e.message);else console.error(`netbeagle: ${e instanceof Error?e.message:String(e)}`),process.exitCode=1});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "netbeagle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "See which process listens on each port, which interfaces reach it, and how everything on your machine connects",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"network",
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
"lsof",
|
|
11
11
|
"interfaces",
|
|
12
12
|
"docker",
|
|
13
|
-
"routes"
|
|
13
|
+
"routes",
|
|
14
|
+
"linux",
|
|
15
|
+
"macos",
|
|
16
|
+
"windows"
|
|
14
17
|
],
|
|
15
18
|
"license": "MIT",
|
|
16
19
|
"author": "Michel Salib",
|