Skip to main content

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
PropertyMeaning
SERVER_URLMatrix's address (https://hub.entrosity.com/matrix).
ENROLLMENT_TOKENA 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 as pending_scope (mismatch). Only guard 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_writes for allowed-sites lists) and fewer than max_writes_per_minute writes in the last minute (default 60, at most 600). Otherwise guard_pending, guard_mismatch, writes_disabled or rate_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​

VariableMeaning
MATRIX_CONNECTOR_DATA_DIRData directory (default %ProgramData%\Entrosity\Matrix Connector, ~/.matrix-connector elsewhere).
MATRIX_LOG_LEVELdebug, info, warn, error.

Files​

In the data directory:

FileWhat
connector.datConnector ID, key and server URL (DPAPI-sealed on Windows).
firewalls.jsonThe applied firewalls (FirewallRef) with their API tokens and credentials versions (tokens DPAPI-sealed).
guard.jsonThe local guard.
journal.jsonThe write journal: each write's intent, written before the write.
state.jsonJobs in progress.
update\Downloaded releases and result.json of the last self-update.
logs\connector.logRotated 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:

ToPortFor
hub.entrosity.com443Matrix API and WebSocket (HTTPS)
FortiGate on the LANits HTTPS admin portFortiOS REST API (/api/v2/cmdb/…)

FortiGate client​

  • Requests go to https://{host}:{port}/api/v2/cmdb/… with Authorization: Bearer <token>, Accept: application/json and always vdom=<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, unless verify_tls is off.
  • Accepted HTTP answers: 200 (and 201 for POST). 404 is not_found, 401/403 auth_failed, anything else an error. The envelope must say status: success, the same http_status, the configured vdom, 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 size and revision on every page and limit_reached present. FortiOS 7.4.11 may answer limit_reached: false with fewer rows than size; the client then follows next_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 of member).

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​

LaneJobsParallel
configmatrix.firewall.apply, matrix.firewall.remove1
readmatrix.firewall.check, matrix.refresh2
policymatrix.policy.set (different rooms in parallel; the same room is busy)4
addressmatrix.address.update, matrix.address.create, matrix.sites.set1
updateupdate_agent1

Timing​

WhatWhen
Rooms reportEvery firewall's report interval (default 30 s, 15–300), and right after each write
Address groups, allowed sitesEvery 5 minutes, on refresh (addresses, all) and after address or sites writes; sent only when their hash changed
HeartbeatEvery minute
Write-time authorizationRight before each write; any answer but allowed: true within 5 seconds blocks it
FortiGate request4 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.

  1. 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_KEY at build time) before downloading anything: a build without the key answers update_unsigned, a bad signature signature_invalid.
  2. 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 task MatrixConnectorUpdate that starts a minute later (its own name, so other Entrosity connectors on the same computer keep theirs).
  3. The task runs msiexec /i <new MSI> /qn /norestart, waits for the EntrosityMatrixConnector service to run and keep running, and otherwise installs the previous MSI again. The task then removes itself.
  4. The outcome is written to update\result.json and logged at the next start (connector update finished); the new version is reported in hello.

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.