Matrix connector
The Matrix connector (entrosity-matrix-connector) is a Go program that
runs as a Windows service on a computer of the school's network. It talks
to Matrix over HTTPS and WebSocket (Matrix connector protocol)
and to the FortiGates' REST API on the LAN. For the user-facing side see
Connectors.
The connector is authoritative for what may be written: every check of the pattern, direction, VDOM, guard and versions happens in it, and fails closed. Its checks are a port of the single-school panel Matrix replaces.
Installation
matrix-connector.msi (WiX, per machine, 64-bit), from the connector's
GitHub releases (check it against matrix-connector.msi.sha256) or
Download connector in Matrix:
msiexec /i matrix-connector.msi /qn ENROLLMENT_TOKEN=<token> SERVER_URL=https://hub.entrosity.com/matrix
| Property | Meaning |
|---|---|
SERVER_URL | Matrix's address (https://hub.entrosity.com/matrix). |
ENROLLMENT_TOKEN | A Matrix connector enrollment token (hidden in the MSI log's property list, but a verbose log still contains the command line: delete it). |
The MSI installs matrix-connector.exe into
%ProgramFiles%\Entrosity\Matrix Connector, enrolls when both properties
are given (a failed enrollment rolls the installation back), and installs
the service EntrosityMatrixConnector (Entrosity Matrix Connector,
LocalSystem, automatic start, restarted on failure). Without the
properties the service waits for matrix-connector enroll.
- Upgrade: run the new MSI; data and the guard are kept.
- Uninstall: removes the service, the program, the enrollment
(
connector.dat), the firewalls with their tokens (firewalls.json),state.json, the journal and the guard; logs stay.
Command line
matrix-connector run [--server URL --token T] [--dev] run (foreground, or as the service); enroll first when given a token
matrix-connector enroll --server URL --token T [--force] enroll this computer and exit
matrix-connector status enrollment state and firewalls with their guard state
matrix-connector guard show [firewall_id] the local guard
matrix-connector guard accept <firewall_id> [--yes] accept a firewall's scope (elevated)
matrix-connector guard set <firewall_id> [--policy-writes on|off] [--address-writes on|off] [--site-writes on|off] [--max-writes-per-minute N]
matrix-connector guard reset <firewall_id> withdraw the acceptance (elevated)
matrix-connector check --config local.json read-only check of a FortiGate
matrix-connector install | uninstall manage the Windows service
matrix-connector version print the version
--dev also drives firewalls of driver simulator
(Trying Matrix with the simulator). enroll
refuses when already enrolled unless --force.
Local guard
guard.json in the data directory holds, per firewall ID:
{
"firewalls": {
"7c0e…": {
"scope": {"host": "192.0.2.1", "port": 443, "vdom": "root",
"source_interface": "Students", "destination_interface": "INTERNET",
"policy_pattern": "…", "hostname_suffix": ".coding.local",
"marker_prefix": "Entrosity Matrix operation"},
"pending_scope": null,
"accepted": true,
"allow_policy_writes": true,
"allow_address_writes": false,
"allow_site_writes": false,
"max_writes_per_minute": 60,
"accepted_at": "…", "updated_at": "…"
}
}
}
- The service only records: the first apply of a firewall stores it
unaccepted (
pending); a later apply with another scope keeps the accepted scope and stores the new one aspending_scope(mismatch). Onlyguard accept(on the computer, elevated) accepts; the server can never change the guard. - A write needs
accepted, the current scope equal to the accepted one, the kind allowed (allow_policy_writes/allow_address_writes/allow_site_writesfor allowed-sites lists) and fewer thanmax_writes_per_minutewrites in the last minute (default 60, at most 600). Otherwiseguard_pending,guard_mismatch,writes_disabledorrate_limited. - The file is read again for every check (no restart needed). On Windows it must be owned by and writable only by SYSTEM and Administrators; elsewhere not writable by group or others. Otherwise it is ignored and every write is refused.
- Removing a firewall (
matrix.firewall.remove) forgets its guard entry. - Allowed-sites writes are off until
guard set <id> --site-writes on; this does not change the scope.allow_service_writes(the removed Entrosity services) in an older file is ignored and dropped at the next change; the firewall stays accepted.
Offline check
matrix-connector check --config local.json is the read-only check of
one FortiGate without Matrix. Its client refuses every request but GET
before it is sent. The configuration file is a firewall reference plus
local files for the token and the CA (unknown keys are refused):
{
"host": "192.0.2.1",
"port": 443,
"vdom": "root",
"source_interface": "Students",
"destination_interface": "INTERNET",
"policy_pattern": "Internet Access for (?P<room>(?:SB[0-9]+|FB|HAC)-[0-9]{3})",
"hostname_suffix": ".coding.local",
"verify_tls": true,
"ca_file": "C:\\Entrosity\\fortigate-ca.pem",
"expected_version": "7.4.11",
"token_file": "C:\\Entrosity\\fortigate-api-token.txt"
}
192.0.2.1 is an example address. token_file holds only the API token
(it is never printed); protect it like a password and delete it when done.
Output:
HTTPS 192.0.2.1:443; VDOM root; TLS verification on
FortiOS v7.4.11
Direction Students → INTERNET: confirmed
Room policies: 8
SB1-102 (building SB1): policy 102, enable
…
Address groups: 8, members: 24
SB1-102-Students address (SB1-102): 3 members, editable
…
Policies with this direction: 10
Warnings:
later_accept_policy: policy 1 "Internet Access for Students" after room SB1-102 accepts the room's computers when the room is disabled (source "all")
Nothing was changed. The effect on traffic was not verified.
It exits non-zero on any error (unreachable, tls, auth_failed,
version_mismatch, direction_invalid, …). It has a five-minute overall
limit.
Environment
| Variable | Meaning |
|---|---|
MATRIX_CONNECTOR_DATA_DIR | Data directory (default %ProgramData%\Entrosity\Matrix Connector, ~/.matrix-connector elsewhere). |
MATRIX_LOG_LEVEL | debug, info, warn, error. |
Files
In the data directory:
| File | What |
|---|---|
connector.dat | Connector ID, key and server URL (DPAPI-sealed on Windows). |
firewalls.json | The applied firewalls (FirewallRef) with their API tokens and credentials versions (tokens DPAPI-sealed). |
guard.json | The local guard. |
journal.json | The write journal: each write's intent, written before the write. |
state.json | Jobs in progress. |
update\ | Downloaded releases and result.json of the last self-update. |
logs\connector.log | Rotated log. Warnings and errors also go to the Application event log (source EntrosityMatrixConnector). |
At start, the connector reports every job that was running when it
stopped: when its journal shows a write that may have been sent, as
unconfirmed with the journaled details (PolicySetResult with
wrote: true, or the AddressOpResult stage); otherwise as
interrupted (nothing was written).
Network
Outbound only:
| To | Port | For |
|---|---|---|
hub.entrosity.com | 443 | Matrix API and WebSocket (HTTPS) |
| FortiGate on the LAN | its HTTPS admin port | FortiOS REST API (/api/v2/cmdb/…) |
FortiGate client
- Requests go to
https://{host}:{port}/api/v2/cmdb/…withAuthorization: Bearer <token>,Accept: application/jsonand alwaysvdom=<vdom>. No proxy from the environment, no redirects, timeouts of 4 seconds (connect) and 8 seconds (total). TLS is verified against the configured CA, or the system roots, unlessverify_tlsis off. - Accepted HTTP answers: 200 (and 201 for POST). 404 is
not_found, 401/403auth_failed, anything else an error. The envelope must saystatus: success, the samehttp_status, the configuredvdom, and, when a version is pinned, that version (version_mismatch). - Errors never contain the token or the raw answer.
- Lists are read 200 at a time (at most 100 pages) and must stay
consistent: the same
sizeandrevisionon every page andlimit_reachedpresent. FortiOS 7.4.11 may answerlimit_reached: falsewith fewer rows thansize; the client then followsnext_idx, which must increase. A list that changes while it is read is refused (invalid_response) and read again next time. - Endpoints:
system/interface,system/zone,system/sdwan(the direction),firewall/policy(rooms; PUT{"status"}only),firewall/address(GET, PUT of the subnet, POST of new /32 and FQDN objects, DELETE of unused Matrix-owned FQDN objects),firewall/addrgrp(GET, PUT ofmember).
What the checks allow
A policy is a room only when its name fully matches the pattern (with a
non-empty room group), its policyid is 1 to 4294967295, its VDOM is the
configured one, srcintf is exactly [{name: <source>}] and dstintf
exactly [{name: <destination>}] (nothing else), and status is enable
or disable. Duplicate policy IDs make the whole read invalid.
A room switch (matrix.policy.set) takes a per-policy lock (busy when
held), reads the policy again (exactly one result with that ID), requires
it to be a room and the same room as the job (room_mismatch), asks
Matrix to authorize (policy.status with the previous and desired
status), and only if the status differs journals the intent, sends
PUT {"status": …} and reads it back (same ID and name, the desired
status; else unconfirmed). A policy already in the desired state is a
success without a write.
Address changes read the room policy, its group, the object and every
use of the object and group across all IPv4 policies and groups, and
refuse anything outside the room (Addresses and groups → What can be
edited). New objects are
ipmask with mask 255.255.255.255 and the comment
<marker_prefix> <operation id>. Group versions are SHA-256 fingerprints
compatible with the old panel's.
Allowed sites change only the members of
the fixed groups Matrix allowed sites and Matrix allowed sites <room>, create fqdn objects matrix-site:<domain>
with the comment <marker_prefix> site, and delete only such objects when
no group or policy uses them any more; any other member is kept and shown
read-only (protocol). A list needs its
group and an ACCEPT policy with the firewall's exact direction and the group
in dstaddr (sites_not_set_up). Such a policy (every dstaddr an
allowed-sites group, or the old Matrix Entrosity services group, which
Matrix never writes) may use the room groups as sources: they
stay editable, and it is not a later_accept_policy. A
matrix.services.sync job (the removed Entrosity services) fails as an
unsupported job type, and the services_enabled / service_domains
fields of an apply are ignored.
Jobs and lanes
| Lane | Jobs | Parallel |
|---|---|---|
config | matrix.firewall.apply, matrix.firewall.remove | 1 |
read | matrix.firewall.check, matrix.refresh | 2 |
policy | matrix.policy.set (different rooms in parallel; the same room is busy) | 4 |
address | matrix.address.update, matrix.address.create, matrix.sites.set | 1 |
update | update_agent | 1 |
Timing
| What | When |
|---|---|
| Rooms report | Every firewall's report interval (default 30 s, 15–300), and right after each write |
| Address groups, allowed sites | Every 5 minutes, on refresh (addresses, all) and after address or sites writes; sent only when their hash changed |
| Heartbeat | Every minute |
| Write-time authorization | Right before each write; any answer but allowed: true within 5 seconds blocks it |
| FortiGate request | 4 s to connect, 8 s in total |
Self-update
Matrix offers a newer version as an update_agent job (component
matrix-connector); how releases get there: Running Entrosity Matrix →
Connector releases.
- The job carries the release (
target) and, when Matrix has it, the MSI of the running version (rollback), each with an Ed25519 signature. The connector checks the signatures against the public key built into it (RELEASE_PUBLIC_KEYat build time) before downloading anything: a build without the key answersupdate_unsigned, a bad signaturesignature_invalid. - It downloads both MSIs into
<data dir>\update\, checks their SHA-256 and size (download_failed,hash_mismatch), and registers a one-shot SYSTEM scheduled taskMatrixConnectorUpdatethat starts a minute later (its own name, so other Entrosity connectors on the same computer keep theirs). - The task runs
msiexec /i <new MSI> /qn /norestart, waits for theEntrosityMatrixConnectorservice to run and keep running, and otherwise installs the previous MSI again. The task then removes itself. - The outcome is written to
update\result.jsonand logged at the next start (connector update finished); the new version is reported inhello.
Connectors built without RELEASE_PUBLIC_KEY do not announce update
and are never offered updates.
Development
make build-local
dist/matrix-connector enroll --server http://localhost:8085/matrix --token <token from Matrix → Connectors>
make dev # run --dev: simulated firewalls
make lint vet test
To exercise the real fortigate driver without a FortiGate, run
go run ./cmd/fortigate-sim --dir /tmp/fgsim (HTTPS on 127.0.0.1:18443;
ca.pem and token.txt in the directory) and point a firewall of driver
fortigate at it. Its control API on 127.0.0.1:18444: GET /state,
GET /writes, POST /fault?match=S&count=N (the next matching writes take
effect but are never answered), DELETE /fault, and POST /cli, which
applies the allowed-sites setup CLI (config firewall addrgrp / config firewall policy with edit, set, append, next, end) like an
administrator would. Like FortiOS, the simulator knows the built-in all
and none addresses, refuses empty groups and refuses to delete an object
that a group or policy uses.
The repository needs entrosity-shared-go next to it and a gitignored
go.work (use . ../entrosity-shared-go) until a shared-go tag contains
proto/matrix. Always check GOOS=windows go vet ./... (make vet).
Tests run the FortiGate client against a test FortiGate (envelopes,
pagination with the 7.4.11 truncation, redirects, proxies, TLS, token
redaction), port the old panel's connector-side tests to the simulator,
and run the whole connector against a fake backend.
Releases: tag vX.Y.Z (a -suffix makes a pre-release) to build the MSI
and EXE and publish them on a GitHub release; every push to main
refreshes the rolling dev pre-release.