Skip to content
Axle-langPublic

About

What every program on this machine is talking to — grouped by process, resolved to owner and AS, each endpoint scored on how it reconnects.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

24 Commits

Folders and files

Repository files navigation

🛰️ netaudit

What every program on this machine is talking to — one window, in real time

Powered by Axle 0.18 Machine readings from gesso Interface by Zaffre Built on smalt

Platform: Windows · Linux Dependencies: none License: MIT

netaudit: a tree of programs, each folded open onto the addresses it is talking to

Written by the program itself — --snap draws one frame and puts it in a file.


The idea · What it sees · Why it sits still · Keys · Building · Reading the source · gesso ↗ · Zaffre ↗ · smalt ↗ · Axle ↗


The idea

A connection list tells you a machine is talking to two hundred addresses. That is not an audit; it is a haystack.

What separates one address from another is not volume but shape:

  • a browser tab opens a burst of connections and stops;
  • an update service reconnects every few hours, raggedly, because its timer drifts;
  • a beacon reconnects every thirty seconds, to the millisecond, forever.

So netaudit is a tree, one row per program, folding open onto the addresses it has been talking to. Every endpoint carries a ring of the instants it came back, and three readings taken from that ring — the median gap, the wander around it, and the steadiness that falls out.

The row says its reading in words:

×214 visits · every 30 s · clockwork

A window just opened has no rhythm to report and says seen once. The readings fill in as the session runs — which is the whole point of a journal that outlives what is live.

The trust score is out of five and always explainable. Enter opens a card listing every signal that applied, with the points it cost or earned. A score nobody can interrogate will be believed blindly or ignored entirely, and both are worse than no score.


What it can and cannot see

There is no URL. Neither Windows nor Linux records the path of an HTTP request anywhere a tool can read, and HTTPS would encrypt it even if it did. Getting one needs a proxy in the middle or a packet capture, and this is neither.

What it answers instead:

Question Where the answer comes from Reading
which program, which protocol, which address and port gesso's socket table (the per-process TCP/UDP tables) POLL
which name is behind the address gesso: the machine's DNS cache, reverse DNS, Team Cymru DNS
who owns the address, and where it is ip-api.com, with gesso's Team Cymru lookup behind it DNS
how many bytes moved gesso's kernel trace (ETW), per process ETW
the short connections polling misses the same trace ETW

Three readings, three badges in the title bar. Each is lit when it is running, and each explains itself when hovered.

ETW lights when gesso's kernel trace is running — which needs an elevated session on Windows and has no Linux trace yet. Its bytes are counted per program, because that is the partition the trace gives: a program's row shows them, and the endpoint rows stay blank rather than have a program total split across sockets. Dark, the badge says why — a blank byte column with nothing to explain it would read as these programs sent nothing, which is the one thing it must never say.

Two things are hidden by default, each with its count on the toggle that reveals it: traffic that never leaves the building (L), and sockets with no peer — listening and UDP (B). Between them they are three quarters of the rows on a working machine, and not one of them answers the question the window is asking.


It refreshes every second and still sits still

The machine underneath changes constantly. The window does not move under your hand. Six rules, applied everywhere:

An endpoint is a journal entry, not a snapshot. A closed connection goes grey and stays five minutes after its last socket is gone, counts intact. A table of what is ESTABLISHED right now would flicker continuously, and would answer the wrong question.
Identity is never an index. The selection is an endpoint key, the folds are group keys, the scroll is pixels. A rebuild cannot move any of them.
The sort is damped, and freezes on hover. A row only overtakes its neighbour by a margin — and while the pointer is over the tree, nothing reorders at all.
Figures are smoothed. A rate carries three quarters of the previous reading.
It repaints on change, not on a clock. Idle, it sleeps in the OS.
Enrichment lands quietly. A row completes in place; it does not jump.

Keys

↑ ↓ move · PgUp PgDn Home End move further
→ ← open and fold a program · 8 fold everything
Enter the full sheet for the selected endpoint · a double click does the same
O open the folder holding the program, binary selected
S next ranking · the Sort field lists them all · / filter
L also show local traffic · B also show listening and UDP sockets
P pause · R or F5 re-read now · C clear the journal
F12 write the window to netaudit.bmp, in the folder it was started from
F1 or ? the key list · Esc close a sheet or clear the filter · Q quit

Space pauses too, except on a row: with the tree focused it folds and unfolds the program, as it does in any tree.

Esc never closes the window: a key pressed to dismiss something must not be able to end the session you were in the middle of.

The window resizes, and the layout follows. The four figures share the width, the tree grows to the bottom, and the badges stay anchored to the right edge. The interface is Zaffre's: rounded surfaces, soft shadows, and a theme made of tokens rather than hand-picked colours.

The Sort field ranks by what it names, and says beside the row count what that ranking weighs. A program ranks by its best endpoint under the same reading, so the program holding the steadiest timer is the one at the top of a rhythm-sorted tree.

A program's chevron folds it; the rest of its row selects it — so a program's totals are readable without closing what you were looking at.

Enter explains. The sheet shows a program's full image path, everything the two lines of a row had to elide, and the score taken apart. A badge in the title says what that reading is doing, and why, when the pointer rests on it.


Where the names and the networks come from

Two sources. Neither needs a key or an account.

ip-api.com/batch — up to a hundred addresses per request: AS number and name, country, city, operator, and the proxy / hosting / mobile flags. One batch in flight, four and a half seconds apart (the service allows fifteen batches a minute), exponential back-off on failure, and every address answered exactly once per session. A batch that never arrived — the network was down, the service said 429 — puts its addresses back in line; it is not read as "every one of them came back empty".

Team Cymru's origin, through gesso — Net::origin asks x.y.z.w.origin.asn.cymru.com and then AS<n>.asn.cymru.com, both TXT, and reads both answers into one AsOrigin (number, prefix, country, registry, name). It is asked once per address, answered or not, so one address the registry cannot place never holds up the rest.

Why the second one is DNS and not HTTP

std::net's HTTP client speaks HTTP/1.1 and not TLS, so an https:// request from an Axle program cannot succeed. A registry fallback over HTTPS would have been one that never once answered — and it would have looked exactly like an address nobody could name. DNS needs no TLS, no key, and no rate limit worth the name.

For the same reason there is no blocklist signal in the score. Every keyless blocklist service is HTTPS-only, and a trust signal that can never fire is worse than an absent one: it reads as evidence of innocence.


Building

The machine layer, gesso, is a submodule, and so are the interface, Zaffre, and the smalt under it, so the clone has to bring all three:

git clone --recursive https://github.com/Axle-lang/netaudit
cd netaudit
axle build
./target/netaudit.exe     # Windows; on Linux the binary is ./target/netaudit

An existing clone that predates one of the submodules: git submodule update --init --recursive.

The only prerequisite is the Axle compiler, v0.18.1 or newer. No SDK to install, no DLL to copy beside the binary, no [link] section to fill in: this program declares no OS library at all. Every call — the process list, the socket tables, the resolver, the kernel trace, revealing a file — is gesso's, and gesso names each library (iphlpapi, kernel32, ntdll, dnsapi, advapi32, shell32, …) in its own tree; the window names gdi32 through smalt. The link line collects each from the declaration that needed it.

On Linux, the same axle build links smalt's X11 backend, so the one thing to install first is its development headers:

sudo apt-get install libx11-dev libasound2-dev

(A Wayland build would use libwayland-dev instead; this manifest leaves smalt's wayland feature off, so X11 is the backend — it runs under Xwayland on a Wayland session.) At run time libX11 and libasound are already on a desktop; nothing is bundled.

Nothing needs elevation for the window as it opens. The connection tables, the resolver cache and the reverse lookups are readable by any process. Run elevated, two things improve: the image paths of services running under another account become readable, so svchost and friends are grouped by path rather than by name; and the ETW badge lights, because a real-time kernel trace needs an administrator or a member of Performance Log Users.

On Linux, an ordinary user sees which process owns only their own sockets. Another user's socket is still read — its address, port, state and traffic are all there — and is grouped under user N with the note that its process is out of sight; running as root names the process instead. Linux has no kernel trace that gesso reads yet, so the ETW badge stays off there.

The resolver cache is read, never queried. On Windows gesso reads each cached name with DNS_QUERY_NO_WIRE_QUERY, so nothing about what this machine looked up leaves it. Linux keeps no resolver cache a program may read, so the DNS badge says so and names come from reverse DNS and Team Cymru instead.

--snap <ms> waits that long, writes netaudit.bmp, and quits: a capture for a report or for a script, with nobody standing over the machine at the right moment. F12 does the same on demand. A third argument says what to open first: --snap 5000 detail is the sheet of the first endpoint, help the key list, and keys:/,g,i,t,h,u,b plays those keys into the window — each letter typed, a name such as Down or Enter pressed — which is how its keyboard is checked without a keyboard.


Reading the source

Everything below is for someone opening the files, not running the binary.

Layout

netaudit
├── axle.toml                  the package, and the gesso, Zaffre and smalt path dependencies
├── src/
│   ├── main.axle              the loop: events in, a frame out, the readings and the two lookups in flight
│   ├── session.axle           what the loop owns: the world, the machine, the queue, the app
│   ├── app.axle               the interaction state, as the models the widgets are bound to
│   ├── fmt.axle               the figures a library cannot format: an address, a rate
│   │
│   ├── sys/                   the machine, as this program reads it
│   │   ├── machine.axle       the readers, as one value
│   │   ├── conn.axle          gesso's socket table, folded into the journal's columns
│   │   ├── procs.axle         gesso's process list, and the image-path cache
│   │   ├── dnscache.axle      gesso's resolver cache, inverted into the host table
│   │   ├── traffic.axle       gesso's kernel trace, as per-process deltas
│   │   └── tiers.axle         which readings are running, and why the others are not
│   │
│   ├── model/                 the journal, and every reading taken off it
│   │   ├── world.axle         the six tables below, as one value
│   │   ├── pool.axle          interned text — nothing here is ever a `string`
│   │   ├── key.axle           the one hash every identity is folded with
│   │   ├── addr.axle          loopback / private / public, and how an address is keyed
│   │   ├── hosts.axle         one row per address: name, network, place, flags
│   │   ├── endpoints.axle     the journal, and the salience the tree ranks by
│   │   ├── rhythm.axle        the arrival ring, and the period and steadiness from it
│   │   ├── groups.axle        one row per program, keyed on the image path
│   │   ├── score.axle         the trust reading, and the nine reasons behind it
│   │   ├── view.axle          the flattened tree: which rows, in what order, how tall
│   │   └── pulse.axle         the four headline figures, sampled once a tick
│   │
│   ├── enrich/                who owns an address, asked over the network
│   │   ├── queue.axle         who gets looked up, how often, and the back-off
│   │   ├── worker.axle        the blocking calls — HTTP and DNS — on a thread of its own
│   │   ├── scan.axle          values out of a JSON response, byte by byte
│   │   └── ipapi.axle         the batch response, folded into rows
│   │
│   └── ui/                    the window, in Zaffre's components; nothing here reads the machine
│       ├── page.axle          the one page: title, figures, filter, tree, status bar
│       ├── journal.axle       the tree as a `DataGrid` sees it: a source over the model's rows
│       ├── figures.axle       what is rebuilt from the model: the figures, the badges, the count
│       ├── live.axle          `Rebuilt`: a child made again whenever the model moves
│       ├── root.axle          the window's own keys, and the sheets that open over the page
│       ├── detail.axle        the endpoint sheet, and the key list
│       ├── actions.axle       open a folder, clear the journal, fold everything
│       └── text.axle          bytes in the pool and the formatting cursor, as `string`
│
├── doc/netaudit.png           this page's screenshot, written by `--snap`
└── vendor/
    ├── gesso                  the machine layer, as a submodule
    └── zaffre                 the interface library, as a submodule
        └── vendor/smalt       and the platform layer under it, the library's own submodule

This program declares no port of its own. Every operating-system call is gesso's; axle.toml names no [port.*], and src/sys/ is shared files that fold gesso's readings into the journal. The window's surface is smalt's. axle ports prints gesso's and smalt's seams; netaudit has none to show.

What it stands on

Three libraries, all Axle's, all submodules.

gesso is the machine: the process list with its image paths, the per-process TCP/UDP socket tables with the process that owns each, the resolver's cache and its PTR/TXT lookups, Team Cymru's origin, the kernel trace, and revealing a binary in the file manager. netaudit declares no OS call of its own; sys/procs, sys/conn, sys/dnscache and sys/traffic fold gesso's readings into the columns and tables the journal has always used.

smalt is the platform: the window, the surface, the event queue, the clock, the byte pool, the slot index, the formatter and the BMP writer. There is exactly one copy of it — the one Zaffre was written against — and axle.toml points this program at that one, so the interface and the readers never disagree about a type.

Zaffre is the interface, written from what this program used to draw by hand: a DataGrid that asks only for the rows in view, Stat, SearchInput, ToggleChip, ScoreMeter, StatusBar, ReasonList, Sheet. The program supplies what a library cannot know — the journal, and what each cell of it says — and Zaffre supplies everything that looks like something.

Four things are worth naming.

The grid keeps what the reader did, and the program keeps the data. The pick is a key, the scroll is pixels, and whether the pointer is over the rows is a flag both sides can read. The journal is re-read every second and the tree is rebuilt from it, and none of the three can come to mean a different row because of that.

The loop sleeps in the OS. Events::wait blocks until an event arrives, an animation needs a frame, or the next reading is due. Idle and paused, this program uses no measurable CPU at all — which a poll-and-sleep loop cannot say, whatever the sleep, and which a tool that measures the machine owes it. The loop is this program's own rather than Zaffre's App::run, because the readings and the two lookups in flight have to be turned over on the same thread that draws.

What changes once a second is made again; what does not is made once. A Stat holds the text it was built with, so the figures, the badges and the row count are each a Rebuilt: a child made afresh when the stamp the loop bumps at each reading has moved. A frame that only animates allocates nothing.

Text is drawn from bytes, and handed to a widget as a string only at the last step. Every name lives in one pool and is referred to by an offset and a length; ui/text.axle is the one door out of it. The path O hands to ShellExecuteW is kept as UTF-16 beside the drawable copy, so a folder with an accent in its name still opens.

What stays here is what a library cannot know: an address in its canonical text form, a rate whose empty case means nothing measured bytes rather than nothing was sent, and the fold the identity indexes are keyed on.

How it is written

Four habits, each turning a class of silent mistake into a compile error or a signature that says what it touches.

What travels together is one value. Session is what the loop owns, World the six model tables, Machine the readings it keeps, Lens what the reader asked the tree to show, and Sighting, Obj and Bytes are the small values a socket, a JSON object and a span of text are. Axle's borrows are second-class (E0513 — a reference lent for a call cannot be kept in a field), so what several widgets hold at once is a Shared<Session>, and what a widget keeps for itself is passed own. The kernel trace is a dyn TraceReader owned by the loop: a class stored in a trait-typed field is not lowered yet, and a local is where it belongs anyway.

Every state is an enum, read by a match with no wildcard — Tier, TierState, Sort, RowKind, Say, Proto, SocketKind, Reach, Look, NameSource, PathState, WhoisKind, Signal. The columns included: they are Reach[] and Look[], not i32[]. The wildcards left are on integers (a TCP state number, an index into a cycle) and on the keys and events this window does not answer. The trust score reads the same nine-arm table the sheet lists, so a signal cannot be scored without being explained.

A function name is the crate's, not the module's. Axle resolves top-level names across the whole crate, so address in ui/detail could not coexist with fmt::address, and the model's View and Reason became Rows and Signal rather than shadow Zaffre's. A class is declared before the code that hands it to a trait object — which is why ui/figures is a file of its own.

There is no unsafe and no extern "C" in this program. Every raw read and every operating-system entry point is gesso's or smalt's; the grep for either in src/ returns nothing. That is what basing the program on gesso bought: the machine layer is the library's problem, and its ports are the library's.

The interface is set in Instrument Sans, which Zaffre embeds; the font is under the SIL Open Font License (vendor/zaffre/assets/fonts/InstrumentSans-OFL.txt).


MIT — see LICENSE.

About

What every program on this machine is talking to — grouped by process, resolved to owner and AS, each endpoint scored on how it reconnects.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages