cleanup: clean up docs/ directory and consent source files
This commit is contained in:
1 parent
ae5e84592d
commit
2eee6696ef
9 files changed
+5
-860
No files matched your search
-178
@@ -1,178 +0,0 @@
|
|||||||
# Consent Prompts
|
|
||||||
|
|
||||||
A program that has no entry in `0:/config/capabilities.toml` starts with no
|
|
||||||
capabilities. For five of them, it can ask the user the first time it needs
|
|
||||||
one, instead of failing. This works much like TCC on macOS: the desktop shows
|
|
||||||
a prompt. For installed programs the answer is remembered; for any other
|
|
||||||
binary it lasts until the process exits.
|
|
||||||
|
|
||||||
| Capability | The prompt says the app wants to... |
|
|
||||||
|-----------------|-----------------------------------------------------------------------|
|
|
||||||
| `storage_admin` | format, partition and mount disks |
|
|
||||||
| `network_admin` | change network settings and join Wi-Fi networks |
|
|
||||||
| `set_time` | change the system clock and time zone |
|
|
||||||
| `display_admin` | change the screen resolution and brightness |
|
|
||||||
| `log_read` | read the system log, which can reveal what other apps and devices are doing |
|
|
||||||
|
|
||||||
No other capability can be asked for. The eligible set is
|
|
||||||
`CAP_CONSENT_ELIGIBLE` in `Api/Syscall.hpp`.
|
|
||||||
|
|
||||||
## How a request flows
|
|
||||||
|
|
||||||
1. A syscall gated on an eligible capability calls
|
|
||||||
`Sched::Consent::Require()` (`kernel/src/Sched/Consent.cpp`) instead of
|
|
||||||
`HasCapability()`. All of these checks sit at the top of their syscall in
|
|
||||||
`Api/Syscall.cpp`, where the thread holds no locks and may block.
|
|
||||||
2. If the caller lacks the capability and is allowed to ask (see below), the
|
|
||||||
kernel queues a request and blocks the calling thread. A second thread of
|
|
||||||
the same process asking for the same capability waits on the same request.
|
|
||||||
3. The session's consent agent, the desktop, picks the request up with
|
|
||||||
`SYS_CONSENT_NEXT` from its main loop. If `0:/config/consent.toml` already
|
|
||||||
holds an answer for this program and capability, the desktop replies
|
|
||||||
without showing anything. Otherwise it opens the prompt.
|
|
||||||
4. `SYS_CONSENT_REPLY` either grants the capability or records a denial. The
|
|
||||||
waiting thread wakes, and its syscall proceeds or returns
|
|
||||||
`SYS_ERR_PERMISSION`.
|
|
||||||
|
|
||||||
If nobody answers within `CONSENT_TIMEOUT_MS` (120 s), the request is denied.
|
|
||||||
If the agent exits, every pending request is denied. If the app exits, its
|
|
||||||
requests are cancelled.
|
|
||||||
|
|
||||||
## Who may ask
|
|
||||||
|
|
||||||
A process may ask for a capability only when all of these hold:
|
|
||||||
|
|
||||||
- The capability is within its **consent ceiling**: the part of its parent's
|
|
||||||
delegable set, recorded at spawn, that is eligible for consent.
|
|
||||||
- The session has a registered agent. Registering (`SYS_CONSENT_REGISTER`)
|
|
||||||
requires `CAP_COMPOSITOR`, so only the desktop can be the agent.
|
|
||||||
- The process is in the agent's session and is not the agent itself.
|
|
||||||
- The agent still holds the capability as delegable.
|
|
||||||
- It has not already been refused that capability during its lifetime.
|
|
||||||
|
|
||||||
These rules have some consequences:
|
|
||||||
|
|
||||||
- **Standard sessions never prompt.** A standard session holds none of the
|
|
||||||
eligible capabilities, so every ceiling under it is empty.
|
|
||||||
- **Console sessions and SSH sessions never prompt.** They have no agent
|
|
||||||
(sshd is not in the desktop's session), so a remote user cannot put a
|
|
||||||
prompt on the local screen.
|
|
||||||
- **A sandboxing launcher still works.** If a parent spawns a child with
|
|
||||||
nothing delegable, the child's ceiling is empty and it cannot ask.
|
|
||||||
- After a refusal the process gets `SYS_ERR_PERMISSION` immediately every
|
|
||||||
time, so it cannot flood the user with prompts.
|
|
||||||
|
|
||||||
## Why this keeps the capability model's invariants
|
|
||||||
|
|
||||||
1. **Authority only narrows down the process tree.** A grant never exceeds
|
|
||||||
the process's consent ceiling, which is exactly what its parent could have
|
|
||||||
given it at spawn. The grant must also be within the agent's own delegable
|
|
||||||
set. Consent-granted capabilities are added only to the effective set,
|
|
||||||
never to the delegable set, so a consented app cannot pass them on to
|
|
||||||
anything it launches. Consent delays a delegation the parent could already
|
|
||||||
have made; it never exceeds one. The kernel re-checks all of this under
|
|
||||||
the scheduler lock when it applies the grant, so a grant can never land on
|
|
||||||
a process slot that has exited and been reused.
|
|
||||||
2. **Capabilities are kernel-owned.** The kernel identifies the program by
|
|
||||||
the path it loaded, never by a user name.
|
|
||||||
|
|
||||||
## Program identity
|
|
||||||
|
|
||||||
A stored answer is keyed on the program's path, and a path identifies a
|
|
||||||
program only if nobody else can replace the file. On the system volume,
|
|
||||||
writing anything under `0:/os` or `0:/apps` requires `CAP_SYSTEM_IMAGE`
|
|
||||||
(`Fs/ProtectedPaths.cpp`), which no session holds. So a program there is
|
|
||||||
**installed**, and its path is a trustworthy identity.
|
|
||||||
|
|
||||||
The kernel decides this, not the desktop: each `ConsentRequest` carries
|
|
||||||
`imageProtected`, computed from the resolved image path with the same rule
|
|
||||||
that guards writes. Paths are normalized by the same resolver the write check
|
|
||||||
uses, so `0:/os/../users/x.elf` is not mistaken for an installed program.
|
|
||||||
|
|
||||||
Any other binary, such as one in a user's home directory or on a data volume,
|
|
||||||
could be swapped for another at the same path. Its answers are never stored:
|
|
||||||
it is asked each time it runs, and the answer lasts until that process exits.
|
|
||||||
|
|
||||||
The same rule applies to static grants. `Sched::Spawn` starts any image
|
|
||||||
outside `0:/os` and `0:/apps` with no capabilities, whatever its launcher
|
|
||||||
asked for, and logs a warning. Otherwise a program with no authority could
|
|
||||||
overwrite a granted binary in, say, a home directory, and then run with the
|
|
||||||
grant the next time the shell or desktop launched it. A program installed
|
|
||||||
elsewhere gets its authority through consent instead. The Capabilities app
|
|
||||||
flags such grants.
|
|
||||||
|
|
||||||
## The prompt
|
|
||||||
|
|
||||||
The prompt is a normal window inside the desktop, not a full-screen overlay.
|
|
||||||
It is placed over the requesting app's window when that app has one, and at
|
|
||||||
the centre of the screen otherwise. It shows:
|
|
||||||
|
|
||||||
- the app's name: its menu label when the path belongs to an installed app,
|
|
||||||
otherwise the file name
|
|
||||||
- what it wants to do
|
|
||||||
- the full path
|
|
||||||
|
|
||||||
The prompt window belongs to the desktop itself (`programs/src/desktop/consent.cpp`),
|
|
||||||
not to the window server, so no process can map its pixels or send it input.
|
|
||||||
`SYS_WINMAP` and `SYS_WINSENDEVENT` require `CAP_COMPOSITOR` anyway. This
|
|
||||||
leaves clickjacking, where another window is raised over the prompt or steals
|
|
||||||
focus just as the user clicks. To prevent that, **Allow** stays disabled
|
|
||||||
until the prompt has been focused and on top for 700 ms without interruption.
|
|
||||||
Losing focus or being covered restarts the delay.
|
|
||||||
|
|
||||||
| Action | Result | Saved? |
|
|
||||||
|--------------------------------|---------------------------------|--------|
|
|
||||||
| Allow (click, once armed) | Granted | Installed programs only |
|
|
||||||
| Don't Allow (click, or Return once armed) | Denied | Installed programs only |
|
|
||||||
| Escape, or the title-bar close | Denied for this process only | No |
|
|
||||||
| Timeout, logout | Denied for this process only | No |
|
|
||||||
|
|
||||||
For a program that is not installed, the prompt also says that the answer
|
|
||||||
applies only until the program exits.
|
|
||||||
|
|
||||||
Only one prompt is shown at a time. Further requests wait in the kernel's
|
|
||||||
queue, which holds up to 16 requests.
|
|
||||||
|
|
||||||
## Stored decisions
|
|
||||||
|
|
||||||
Decisions are stored in `0:/config/consent.toml`:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[decision.1]
|
|
||||||
path = "0:/apps/wifitool/wifitool.elf"
|
|
||||||
capability = "network_admin"
|
|
||||||
decision = "allow"
|
|
||||||
```
|
|
||||||
|
|
||||||
A decision applies when the path and the capability match. Only installed
|
|
||||||
programs are ever stored, and the desktop looks up only requests the kernel
|
|
||||||
marked `imageProtected`. A new answer for the same path and capability
|
|
||||||
replaces the old one. Updating an installed program keeps its decisions,
|
|
||||||
because installing it already required `CAP_SYSTEM_IMAGE`. The file is a
|
|
||||||
protected path that requires `CAP_USER_ADMIN` to write, the same as
|
|
||||||
`capabilities.toml`: anything that can edit it could grant itself access.
|
|
||||||
|
|
||||||
The **Consent** tab of the Capabilities app lists the stored decisions and can
|
|
||||||
**Forget** one, so the program is asked again next time. A copy that is
|
|
||||||
already running keeps a granted capability until it exits.
|
|
||||||
|
|
||||||
## Syscalls
|
|
||||||
|
|
||||||
| Number | Name | Caller | Purpose |
|
|
||||||
|--------|------------------------|--------|---------------------------------------------|
|
|
||||||
| 191 | `SYS_CONSENT_REGISTER` | agent | Become the session's consent agent (needs `CAP_COMPOSITOR`) |
|
|
||||||
| 192 | `SYS_CONSENT_NEXT` | agent | Take the oldest queued request (`ConsentRequest`) |
|
|
||||||
| 193 | `SYS_CONSENT_REPLY` | agent | `CONSENT_ALLOW` / `CONSENT_DENY` a delivered request |
|
|
||||||
|
|
||||||
## Not covered yet
|
|
||||||
|
|
||||||
- **Elevation for standard users**, for example entering an administrator's
|
|
||||||
password in the prompt. This needs a different source of authority, because
|
|
||||||
a standard session has nothing to delegate.
|
|
||||||
- **Console sessions.** A text prompt in the terminal could be faked by
|
|
||||||
anything that prints to it.
|
|
||||||
- **Per-user decisions.** The table is system-wide, like the grants.
|
|
||||||
- **Revoking from a running process.** Forgetting a decision only affects
|
|
||||||
the next launch.
|
|
||||||
- **App-supplied reasons.** The prompt describes the capability in the
|
|
||||||
system's own words. A program cannot add text of its own.
|
|
||||||
-168
@@ -1,168 +0,0 @@
|
|||||||
# CPU Power and Thermal Management
|
|
||||||
|
|
||||||
MontaukOS manages CPU frequency, idle states, and package temperature on
|
|
||||||
Intel hardware. Without this, a modern many-core laptop part (the reference
|
|
||||||
target is a TUXEDO Gemini Gen2 with an i9-13900HX, 24 cores / 32 threads)
|
|
||||||
runs at whatever P-state the firmware left it, never drops below C1 when
|
|
||||||
idle, and heats until the hardware throttles at TjMax - or until the
|
|
||||||
embedded controller intervenes (on Clevo boards: forced full-volume beeping,
|
|
||||||
then a hard power-off).
|
|
||||||
|
|
||||||
Everything below is feature-gated by CPUID, so virtual machines (KVM masks
|
|
||||||
the power bits in CPUID.06H) and non-Intel CPUs skip it safely.
|
|
||||||
|
|
||||||
## Components
|
|
||||||
|
|
||||||
### HWP frequency scaling (`Hal/CpuPower.cpp`)
|
|
||||||
|
|
||||||
`Hal::CpuPower::InitializeBsp()` enables Hardware P-states
|
|
||||||
(IA32_PM_ENABLE), reads the performance range from IA32_HWP_CAPABILITIES,
|
|
||||||
and programs IA32_HWP_REQUEST with:
|
|
||||||
|
|
||||||
- min = lowest performance level, max = highest
|
|
||||||
- desired = 0 (fully autonomous hardware frequency selection)
|
|
||||||
- EPP = 0x80 (balanced; same as Linux "balance_performance")
|
|
||||||
|
|
||||||
This is the same mechanism intel_pstate uses on Linux and is the single
|
|
||||||
biggest thermal win: the CPU idles near its minimum frequency and ramps
|
|
||||||
only while there is work. APs apply the same request in `ApEntry` via
|
|
||||||
`InitializeAp()` (HWP request MSRs are per-logical-CPU).
|
|
||||||
|
|
||||||
CPUs with EPB but no HWP get IA32_ENERGY_PERF_BIAS = 6 (balanced) instead.
|
|
||||||
|
|
||||||
### C1E promotion
|
|
||||||
|
|
||||||
MSR_POWER_CTL bit 1 lets HLT-idle drop core voltage/frequency to minimum
|
|
||||||
(C1E) instead of parking at the current ratio. Set per-core on the BSP and
|
|
||||||
every AP. Gated on the DTS CPUID bit as a real-hardware heuristic, because
|
|
||||||
MSR_POWER_CTL is not architectural and hypervisors may not emulate it.
|
|
||||||
|
|
||||||
### Thermal governor (`Hal/CpuPower.cpp`)
|
|
||||||
|
|
||||||
A closed-loop passive-cooling controller on the BSP, stepped from
|
|
||||||
`Sched::RunBspMaintenance()` (reached from both the idle loop and the
|
|
||||||
timer tick, so it keeps running under full load). Every 500 ms it reads
|
|
||||||
the package digital thermal sensor (IA32_PACKAGE_THERM_STATUS readout
|
|
||||||
relative to TjMax from MSR_TEMPERATURE_TARGET) and adjusts the HWP
|
|
||||||
performance ceiling:
|
|
||||||
|
|
||||||
- **>= 85 C (passive):** step the ceiling down 2 ratio units (~200 MHz)
|
|
||||||
- **>= 94 C (hot):** step down 8 units per interval
|
|
||||||
- **<= 78 C (resume):** step back up 1 unit per interval
|
|
||||||
- between resume and passive: hold (hysteresis)
|
|
||||||
|
|
||||||
The ceiling propagates through a policy epoch counter: each CPU compares
|
|
||||||
the epoch in `Sched::Tick()` (and the AP idle loop) against the last one
|
|
||||||
it applied and rewrites its own IA32_HWP_REQUEST when it changed, since
|
|
||||||
MSR writes only affect the executing core. APs tick every 10 ms, so a
|
|
||||||
throttle decision reaches all cores within one tick.
|
|
||||||
|
|
||||||
Throttle engage/release transitions are logged (WARNING/OK, "CpuPower").
|
|
||||||
|
|
||||||
### Deep AP idle (`ACPI/CpuIdle.cpp`)
|
|
||||||
|
|
||||||
APs use deep MWAIT C-states while idle; C6 power-gates the core entirely,
|
|
||||||
which is where most of the idle-heat win lives on many-core parts.
|
|
||||||
|
|
||||||
The hint comes from ACPI `_CST` FFH entries (the GAS address low byte is
|
|
||||||
the MWAIT hint, same convention as Linux), validated against the CPUID.05H
|
|
||||||
sub-state enumeration and capped at the C6 family (0x2F). With no usable
|
|
||||||
`_CST`, the deepest CPUID-enumerated hint (up to C6) is used.
|
|
||||||
|
|
||||||
**The BSP stays in C1 (HLT).** On some laptops deep MWAIT states freeze
|
|
||||||
the LAPIC timer even though ARAT is advertised, and the BSP's periodic
|
|
||||||
1 ms tick is the system timekeeper (see the comment in
|
|
||||||
`Timekeeping::IdleOnce`). APs have no timekeeping duty: an idle AP is
|
|
||||||
woken by the reschedule IPI, and its coarse tick resumes on wake.
|
|
||||||
|
|
||||||
### S3 wake
|
|
||||||
|
|
||||||
IA32_PM_ENABLE, HWP requests, and MSR_POWER_CTL do not survive suspend.
|
|
||||||
`Hal::CpuPower::ReapplyAfterWake()` runs in the S3 resume path, redoes the
|
|
||||||
BSP setup, and bumps the policy epoch so every AP re-applies its HWP
|
|
||||||
request (and re-checks C1E) on its next tick.
|
|
||||||
|
|
||||||
### Battery (`ACPI/Battery.cpp`, `ACPI/EmbeddedController.cpp`)
|
|
||||||
|
|
||||||
Battery state comes from the ACPI Control Method Battery
|
|
||||||
(`_HID PNP0C0A`) and, for the AC adapter, `ACPI0003`:
|
|
||||||
|
|
||||||
- `_STA` bit 4 says whether a pack is installed. A laptop with the pack
|
|
||||||
removed still has the device, which is why "has a battery device" and
|
|
||||||
"has a battery" are separate answers.
|
|
||||||
- `_BIX` (or `_BIF` on older firmware) gives the power unit (mWh vs mAh),
|
|
||||||
the design capacity, and the last full charge capacity.
|
|
||||||
- `_BST` gives the charge/discharge state, the present rate, the remaining
|
|
||||||
capacity and the present voltage.
|
|
||||||
- `_PSR` on the adapter says whether it is plugged in.
|
|
||||||
|
|
||||||
Percentage is remaining / last-full, clamped to 100 and falling back to the
|
|
||||||
design capacity when firmware reports last-full as zero. Results are cached
|
|
||||||
for two seconds so that a panel repaint does not turn into EC traffic.
|
|
||||||
|
|
||||||
Almost all of this reaches the CPU through the **embedded controller**.
|
|
||||||
`Hal::EmbeddedController` finds the EC's command/data port pair from the
|
|
||||||
ECDT table when one exists, and otherwise from `_CRS` on the `PNP0C09`
|
|
||||||
device, then drives the polled read/write protocol (`0x80`/`0x81` with
|
|
||||||
OBF/IBF handshaking). It is wired into the AML interpreter as the
|
|
||||||
`EmbeddedControl` region space, so any `OperationRegion` in the DSDT that
|
|
||||||
targets the EC now works -- battery methods being the first user.
|
|
||||||
|
|
||||||
Supporting `_BST` also needed real package element storage in the AML
|
|
||||||
interpreter. Firmware overwhelmingly builds its result package with
|
|
||||||
`Store(x, Index(BST0, n))`, which requires `Index()` to name a writable
|
|
||||||
element rather than yield a value. `AML::PackageStore` provides that: a
|
|
||||||
persistent region for packages declared with `Name()`, and a transient ring
|
|
||||||
rewound at each top-level evaluation for packages built inside a method, so
|
|
||||||
polling `_BST` cannot exhaust it.
|
|
||||||
|
|
||||||
## Diagnostics
|
|
||||||
|
|
||||||
`SYS_POWERINFO` (149) fills a `PowerInfo` struct: package temperature,
|
|
||||||
TjMax, HWP state and performance range, the governor's current ceiling,
|
|
||||||
CPUID base/turbo frequency, the measured average active frequency
|
|
||||||
(APERF/MPERF over the 500 ms governor window), and the AP idle MWAIT hint.
|
|
||||||
Returns -1 when the CPU exposes no power features (e.g. under QEMU).
|
|
||||||
|
|
||||||
`SYS_BATTERYINFO` (188) fills a `BatteryInfo` struct: percentage, charge
|
|
||||||
state, AC presence, capacities, present rate, voltage and an estimated time
|
|
||||||
remaining. It returns -1 on a machine with no battery device at all, which
|
|
||||||
is how the desktop panel decides not to draw an indicator. Like
|
|
||||||
`SYS_POWERINFO` it is ungated read-only telemetry: the panel indicator has
|
|
||||||
to work in an unprivileged session, which holds no device capability to
|
|
||||||
spend on it.
|
|
||||||
|
|
||||||
The `power` program (`programs/src/power`) prints both:
|
|
||||||
|
|
||||||
```
|
|
||||||
power one-shot snapshot
|
|
||||||
power watch 1 Hz samples for 60 s (temp, avg freq, ceiling)
|
|
||||||
power watch 300 same, for 300 s
|
|
||||||
```
|
|
||||||
|
|
||||||
Boot log lines to look for (Kernel Log app):
|
|
||||||
|
|
||||||
```
|
|
||||||
[CpuPower] HWP enabled: perf range 4-54, EPP=0x80
|
|
||||||
[CpuPower] Thermal governor armed: TjMax=100C, passive=85C, hot=94C, ...
|
|
||||||
[CpuIdle] AP deep idle: MWAIT hint 0x20
|
|
||||||
[EC] Embedded Controller at cmd=66 data=62
|
|
||||||
[Battery] ACPI battery at \_SB.PCI0.LPCB.EC0.BAT0 (AC adapter present)
|
|
||||||
```
|
|
||||||
|
|
||||||
The desktop panel shows a battery icon left of the Wi-Fi and network icons,
|
|
||||||
using the icon pack's `battery-level-N[-charging]-symbolic` glyphs (one per
|
|
||||||
10% step, reloaded only when the step changes). Clicking it opens a popout
|
|
||||||
with the percentage in numbers, the charge state, time remaining and raw
|
|
||||||
capacity. The icon is absent entirely when `SYS_BATTERYINFO` fails, so a
|
|
||||||
desktop machine shows nothing there.
|
|
||||||
|
|
||||||
## Tuning
|
|
||||||
|
|
||||||
The governor constants live at the top of `Hal/CpuPower.cpp`
|
|
||||||
(`PassiveTempC`, `HotTempC`, `ResumeTempC`, `GovernorIntervalMs`, step
|
|
||||||
sizes) and were chosen to keep the package well below both TjMax (100 C
|
|
||||||
on the i9-13900HX) and the Clevo EC's emergency thresholds. If the fans
|
|
||||||
still spin up aggressively under sustained load, lower `PassiveTempC`
|
|
||||||
first; if builds feel throttled while cool, raise the recovery rate
|
|
||||||
instead of the trip points.
|
|
||||||
-100
@@ -1,100 +0,0 @@
|
|||||||
to-do: rewrite & convert to html for docs pages
|
|
||||||
|
|
||||||
# SSH
|
|
||||||
|
|
||||||
MontaukOS ships an SSH-2 server, `sshd`, that gives a remote client a shell on
|
|
||||||
the machine. It speaks enough of the protocol to interoperate with stock
|
|
||||||
OpenSSH clients:
|
|
||||||
|
|
||||||
```
|
|
||||||
ssh dan@montauk-box interactive shell
|
|
||||||
ssh dan@montauk-box 'ls 0:/' run one command
|
|
||||||
```
|
|
||||||
|
|
||||||
Remote access is **off by default**. Turn it on from Settings > SSH Server,
|
|
||||||
which flips `services.ssh.enabled` in `0:/config/init.toml`; `init` starts the
|
|
||||||
daemon on the next boot.
|
|
||||||
|
|
||||||
Two gates stand in front of a login: the account must exist in the MontaukOS
|
|
||||||
user database with a valid password, and it must be listed in the `[allow]`
|
|
||||||
table of `0:/config/ssh.toml`. A user absent from that table is refused even
|
|
||||||
with the right password. The Settings applet maintains the table; `sshd` never
|
|
||||||
writes it.
|
|
||||||
|
|
||||||
## Layout
|
|
||||||
|
|
||||||
```
|
|
||||||
programs/src/sshd/
|
|
||||||
main.cpp transport framing, key exchange, auth, session/channel loop
|
|
||||||
crypto.hpp host key storage, DH group 14 arithmetic, cipher/MAC wrappers
|
|
||||||
programs/src/sshserver/
|
|
||||||
main.cpp Settings applet: enable the service, edit the allow table
|
|
||||||
programs/include/montauk/ssh.h
|
|
||||||
shared policy helpers used by both
|
|
||||||
```
|
|
||||||
|
|
||||||
The crypto primitives (SHA-256, HMAC, AES-CTR, RSA signing, the DRBG) come from
|
|
||||||
BearSSL. The modular exponentiation for Diffie-Hellman is in `crypto.hpp`
|
|
||||||
because BearSSL exposes no public bignum API.
|
|
||||||
|
|
||||||
## What it implements
|
|
||||||
|
|
||||||
| | |
|
|
||||||
|---|---|
|
|
||||||
| Key exchange | `diffie-hellman-group14-sha256` |
|
|
||||||
| Host key | `rsa-sha2-256`, 2048-bit, generated on first start |
|
|
||||||
| Cipher | `aes128-ctr` |
|
|
||||||
| MAC | `hmac-sha2-256` |
|
|
||||||
| Compression | none |
|
|
||||||
| Auth | password only |
|
|
||||||
|
|
||||||
Client-initiated rekeying works, so long sessions survive OpenSSH's 1 GiB /
|
|
||||||
one-hour rekey threshold. Channel windows are tracked in both directions, so
|
|
||||||
sessions that move more than a window's worth of data keep flowing.
|
|
||||||
|
|
||||||
## What it does not
|
|
||||||
|
|
||||||
No public key authentication, no port or X11 forwarding, no SFTP or SCP. One
|
|
||||||
session channel per connection, and connections are served one at a time --
|
|
||||||
there is no `fork`, so a second client waits for the first to finish. A
|
|
||||||
pre-authentication timeout keeps an idle peer from holding the daemon shut.
|
|
||||||
|
|
||||||
`exec` requests are run by typing the command into an interactive shell rather
|
|
||||||
than executing it directly, so the prompt and the echoed command come back
|
|
||||||
mixed into the output. This is why `scp` and friends will not work as-is.
|
|
||||||
|
|
||||||
## The host key
|
|
||||||
|
|
||||||
Generated on first start and written to `0:/config/ssh_hostkey.toml`, separate
|
|
||||||
from the policy file so the unprivileged Settings applet -- which rewrites
|
|
||||||
`ssh.toml` whenever the allow table changes -- never round-trips private key
|
|
||||||
material.
|
|
||||||
|
|
||||||
That separation is hygiene, not protection: **the filesystem has no permission
|
|
||||||
model**, so any local process can read the host key. Closing that hole needs
|
|
||||||
FS-level access control, at which point the key file should be restricted to
|
|
||||||
the account `sshd` runs as. A key found in an older `ssh.toml` is migrated to
|
|
||||||
the new file on first start so the host key does not change under existing
|
|
||||||
clients.
|
|
||||||
|
|
||||||
## Debugging
|
|
||||||
|
|
||||||
`sshd` calls `montauk::print`, which reaches the boot console only. **It has no
|
|
||||||
usable diagnostics once the desktop is up**, pending the unified syslog.
|
|
||||||
|
|
||||||
Worth understanding, because it catches out anything written as a daemon:
|
|
||||||
`montauk::print` is `SYS_PRINT`, which writes the kernel *terminal*, not the
|
|
||||||
kernel *log*. Only in-kernel `KernelLogStream` writes raise `g_kernelLogDepth`,
|
|
||||||
and only those append to the ring buffer `SYS_LOG` reads -- so daemon output
|
|
||||||
never shows up in `klog`. On top of that, `Sys_Print` returns early once
|
|
||||||
`g_suppressKernelLog` is set, which the desktop does at startup, so the output is
|
|
||||||
discarded outright from then on. `init` spawns services with `spawn` rather than
|
|
||||||
`spawn_redir`, so there is no stream to capture either.
|
|
||||||
|
|
||||||
Until there is somewhere for it to go, debugging a handshake means watching the
|
|
||||||
boot console, or temporarily pointing the print calls at a file.
|
|
||||||
|
|
||||||
Failures print a numeric code -- `key exchange failed (N)`, `authentication
|
|
||||||
failed (N)` -- where N identifies the step; see the `kex_error` assignments in
|
|
||||||
`main.cpp`. Running the client with `ssh -vvv` and lining its trace up against
|
|
||||||
those codes is usually the fastest way to find where a handshake diverged.
|
|
||||||
-395
@@ -1,395 +0,0 @@
|
|||||||
to-do: rewrite & convert to html for docs pages
|
|
||||||
|
|
||||||
# Wi-Fi
|
|
||||||
|
|
||||||
MontaukOS drives Intel AX210-family adapters (the reference part is the AX211).
|
|
||||||
The driver scans, joins open and WPA2/WPA3-PSK networks, and presents itself to
|
|
||||||
the network stack as an ordinary Ethernet interface, so `dhcp`, `ping`, `nslookup`
|
|
||||||
and anything speaking sockets work over Wi-Fi exactly as they do over a cable.
|
|
||||||
|
|
||||||
```
|
|
||||||
wifi scan list nearby networks
|
|
||||||
wifi connect <ssid> <passphrase> join one
|
|
||||||
dhcp pick up an address
|
|
||||||
wifi status what you are connected to
|
|
||||||
wifi saved / wifi forget <ssid> networks remembered for next time
|
|
||||||
```
|
|
||||||
|
|
||||||
There is a graphical path too: a Wi-Fi entry in the desktop panel and a
|
|
||||||
Wi-Fi tab in the Network app. See "The desktop side" below.
|
|
||||||
|
|
||||||
## Layout
|
|
||||||
|
|
||||||
```
|
|
||||||
kernel/src/Drivers/Net/Wifi/
|
|
||||||
IwxTrans.cpp PCIe transport: MMIO, MSI-X, DMA rings, firmware boot,
|
|
||||||
host commands, RX processing, frame TX, key installation
|
|
||||||
IwxFw.cpp .ucode / .pnvm TLV parsing
|
|
||||||
IwxMvm.cpp post-ALIVE init, NVM, UMAC scan, RX dispatch
|
|
||||||
IwxConnect.cpp MLME: contexts, authenticate, associate, 802.11 <-> Ethernet
|
|
||||||
Wpa.cpp WPA2/WPA3-PSK supplicant (EAPOL 4-way + group rekey)
|
|
||||||
Ieee80211.hpp frame, element and RSN constants
|
|
||||||
Wifi.cpp subsystem facade: probe, scan table, syscalls, netif hooks
|
|
||||||
kernel/src/Libraries/Crypto.cpp SHA-1/SHA-256, HMAC, PBKDF2, AES, CMAC
|
|
||||||
kernel/src/Net/NetIf.cpp interface registry the Ethernet layer uses
|
|
||||||
```
|
|
||||||
|
|
||||||
## Joining a network
|
|
||||||
|
|
||||||
`SYS_WIFI_CONNECT` blocks until the link is up or the attempt fails, and
|
|
||||||
returns a `WIFI_ERR_*` code the `wifi` tool turns into a specific message
|
|
||||||
(wrong passphrase, unsupported security, AP out of range, and so on).
|
|
||||||
|
|
||||||
The sequence:
|
|
||||||
|
|
||||||
1. **Look up the BSS.** The SSID is matched against the scan table, strongest
|
|
||||||
signal first. If it is not there, one scan is run automatically and the
|
|
||||||
lookup retried, so `wifi connect` works without scanning first.
|
|
||||||
2. **Negotiate ciphers.** The AP's RSN element (kept verbatim in the scan
|
|
||||||
table) picks the pairwise cipher and AKM. CCMP is preferred over GCMP,
|
|
||||||
plain PSK over PSK-SHA256.
|
|
||||||
3. **Derive the PMK.** PBKDF2-HMAC-SHA1 over the passphrase with the SSID as
|
|
||||||
salt, 4096 iterations. A 64-character hex string is taken as a raw PSK
|
|
||||||
instead.
|
|
||||||
4. **Bring up firmware contexts.** PHY, MAC, binding and station, then one TX
|
|
||||||
queue on the management TID.
|
|
||||||
5. **Authenticate and associate.** Open-system authentication, then an
|
|
||||||
association request carrying the SSID, supported rates and, for encrypted
|
|
||||||
networks, the RSN element the supplicant built. Both are retransmitted up
|
|
||||||
to four times at 400 ms.
|
|
||||||
6. **Run the 4-way handshake.** EAPOL-Key messages 1-4, then the pairwise key
|
|
||||||
and the group key go into the firmware with `ADD_STA_KEY`.
|
|
||||||
7. **Report the link up.** Only now does `NetIf` see `wlan0` as usable.
|
|
||||||
|
|
||||||
## The data path
|
|
||||||
|
|
||||||
Once associated the driver translates between 802.11 and Ethernet II:
|
|
||||||
|
|
||||||
- **TX** - an Ethernet frame becomes a to-DS 802.11 data header plus an
|
|
||||||
RFC 1042 LLC/SNAP shim carrying the EtherType. The protected bit is set
|
|
||||||
once keys are installed and the firmware does the CCMP encryption.
|
|
||||||
- **RX** - the firmware decrypts and strips the MIC but leaves the 8-byte
|
|
||||||
CCMP header, which is skipped; the LLC/SNAP shim is replaced by an Ethernet
|
|
||||||
header built from addresses 1 and 3. EAPOL frames are diverted to the
|
|
||||||
supplicant instead.
|
|
||||||
|
|
||||||
One TX queue carries management frames, EAPOL and non-QoS data. The firmware
|
|
||||||
maps non-QoS data onto the management TID anyway, and the driver never
|
|
||||||
negotiates block-ack sessions that would need per-TID queues.
|
|
||||||
|
|
||||||
### Threading
|
|
||||||
|
|
||||||
The RX path runs under `IwxProcessEvents()`'s reentrancy guard and **must not
|
|
||||||
send a host command** - the command's completion is pumped by the very
|
|
||||||
function it would be re-entering. Anything needing a command (post-association
|
|
||||||
context updates, key installation, and therefore the whole EAPOL handshake) is
|
|
||||||
queued and applied from `IwxConnectService()`, which every idling core calls
|
|
||||||
and which is itself serialized. Transmitting is safe from either context: it
|
|
||||||
only writes a descriptor and rings the doorbell.
|
|
||||||
|
|
||||||
### Never wait on the clock under a spinlock
|
|
||||||
|
|
||||||
`kcp::Spinlock::Acquire()` does `cli`, and `Timekeeping::GetMilliseconds()` is
|
|
||||||
driven by the APIC timer interrupt. A wall-clock timeout inside a spinlock
|
|
||||||
therefore cannot expire: the counter never advances, the loop never ends, and
|
|
||||||
the machine locks solid with interrupts off - no mouse, no keyboard, no
|
|
||||||
scheduler. This is the same trap `ApicTimer.cpp` documents for the idle path.
|
|
||||||
|
|
||||||
`IwxSendCmd` holds a lock across a wait for the firmware's reply, so that lock
|
|
||||||
is a `kcp::Mutex` (which keeps interrupts enabled), and the wait carries a spin
|
|
||||||
cap as well as the clock check so it terminates even if the clock is somehow
|
|
||||||
stuck. It also bails immediately once the firmware is known to have asserted,
|
|
||||||
because nothing after that will ever be answered.
|
|
||||||
|
|
||||||
A firmware command that goes unanswered now dumps the firmware's own error
|
|
||||||
table on the first timeout - that names the command that asserted - and gives
|
|
||||||
up on the adapter after three, rather than stalling for seconds per command.
|
|
||||||
|
|
||||||
### Sample the clock after the work, not before it
|
|
||||||
|
|
||||||
`IwxConnectService()` sends host commands, and every one of them is a round
|
|
||||||
trip to the adapter that takes real milliseconds. Each step also stamps the
|
|
||||||
timestamp its deadline is measured from - `EnterState()` sets
|
|
||||||
`g_stateEnteredMs`, transmitting sets `g_lastTxMs`. A `now` read at the top of
|
|
||||||
the pass is therefore *older* than the stamps it is about to be compared
|
|
||||||
against, and because these are unsigned counters, `now - g_stateEnteredMs`
|
|
||||||
wraps to about 2^64 instead of going negative. Every deadline in the pass then
|
|
||||||
reads as long expired.
|
|
||||||
|
|
||||||
The symptom was a join that failed the instant it succeeded:
|
|
||||||
|
|
||||||
```
|
|
||||||
WiFi: [OK] Associated, AID 3
|
|
||||||
WiFi: [WARNING] Connection failed: timed out while joining the network
|
|
||||||
WiFi: [INFO] EAPOL frame received (99 bytes)
|
|
||||||
```
|
|
||||||
|
|
||||||
The two post-association context commands moved the clock forward, the timeout
|
|
||||||
check compared a stale `now` against the `g_stateEnteredMs` they had just set,
|
|
||||||
and the contexts came down before the access point's first EAPOL frame could
|
|
||||||
arrive - which is why message 1 shows up *after* the failure. So the clock is
|
|
||||||
read only once the command-sending work in the pass is done, and the
|
|
||||||
comparisons go through `Elapsed()`, which refuses to underflow.
|
|
||||||
|
|
||||||
### Teardown unwinds contexts in the order they depend on each other
|
|
||||||
|
|
||||||
Firmware 89 asserts when a context is taken away while another still points at
|
|
||||||
it, and the assert names the command rather than the reason. Three of these
|
|
||||||
have been hit so far:
|
|
||||||
|
|
||||||
| UMAC error | Cause |
|
|
||||||
|---|---|
|
|
||||||
| 0x2010330F | PHY binding and link activation folded into one LINK_CONFIG |
|
|
||||||
| 0x2010330E | link removed while its station still existed |
|
|
||||||
| 0x2000320F | link deactivated while the MAC was still marked associated |
|
|
||||||
|
|
||||||
The last one is the teardown side of the same rule. While
|
|
||||||
`MAC_CONFIG.is_assoc` is set, the firmware's MAC context owns the link carrying
|
|
||||||
the BSS, so `TearDown()` sends a `MAC_CONFIG` MODIFY clearing `is_assoc` first,
|
|
||||||
and only then removes the station, deactivates the link, removes the link,
|
|
||||||
removes the MAC and drops the PHY context - the order
|
|
||||||
`iwl_mvm_mld_vif_cfg_changed_station` and the paths below it use on the way
|
|
||||||
down. `tests/wifi/ap_mlme.py` pins that order.
|
|
||||||
|
|
||||||
### A bad access point must not become a bad computer
|
|
||||||
|
|
||||||
Three separate defects turned "the Wi-Fi connection went wrong" into "the whole
|
|
||||||
machine went wrong". They are worth keeping straight because they have nothing
|
|
||||||
to do with each other beyond sharing a trigger.
|
|
||||||
|
|
||||||
**The command wait is a busy spin on a core the scheduler has been told not to
|
|
||||||
touch.** `ServiceEvents()` runs from `ApicTimer::ServiceDeferredWork()`, which
|
|
||||||
sets `cpu->reservedForKernelWork` so a bottom half holding a process-context
|
|
||||||
mutex cannot be preempted into the process that would wait on it. The
|
|
||||||
scheduler honours that by refusing to place any process on that core and by
|
|
||||||
skipping it for the reschedule IPI, and on the BSP `RunBspMaintenance()` - the
|
|
||||||
thing that wakes sleeping processes - is not reached until the pass returns.
|
|
||||||
`IwxSendCmd` then waits for the firmware by spinning on `IwxDelayUs`, up to a
|
|
||||||
second. One pass of `TearDown()` is eight commands. Against an adapter that has
|
|
||||||
stopped answering, that pinned a core for the better part of ten seconds. The
|
|
||||||
symptom is not a Wi-Fi symptom at all: the cursor crawls, windows stop
|
|
||||||
repainting, everything stutters.
|
|
||||||
|
|
||||||
So a service pass now carries a budget - `IwxBeginServicePass()` /
|
|
||||||
`IwxEndServicePass()` - of roughly one command's worth of waiting in total.
|
|
||||||
Commands that do not fit are not sent at all (an abandoned command still holds
|
|
||||||
its ring slot, and a late answer would be misread as the *next* command's
|
|
||||||
completion) and are retried on the next trip round the idle loop. The bring-up
|
|
||||||
path does not bracket itself: it reserves its CPU deliberately and has nothing
|
|
||||||
to starve, so it keeps the full per-command timeout.
|
|
||||||
|
|
||||||
**Counting consecutive failures never fires on the failure that matters.** The
|
|
||||||
give-up rule was three unanswered commands in a row, with any success resetting
|
|
||||||
the count. An adapter that answers some commands and drops others - which is
|
|
||||||
exactly what a marginal link leaves the firmware doing - therefore never
|
|
||||||
reached the cutoff, while every drop still cost a full timeout. The stall was
|
|
||||||
not a one-off; it repeated indefinitely, and the driver never concluded
|
|
||||||
anything was wrong. It is a leaky bucket now: a failure adds one, a success
|
|
||||||
drains one, so a firmware failing even a fraction of its commands trips the
|
|
||||||
cutoff in bounded time.
|
|
||||||
|
|
||||||
**`IwxFwState::Error` was a one-way door.** Nothing anywhere cleared it. A
|
|
||||||
single firmware assert left `StartJoin()` returning `WIFI_ERR_NO_ADAPTER` -
|
|
||||||
which the user sees as *no adapter is ready* - for the rest of the uptime, and
|
|
||||||
the only way back was a reboot. Disconnecting and reconnecting could not help:
|
|
||||||
there was nothing to reconnect with. Since `IwxReadFirmware()` is idempotent
|
|
||||||
and the parsed image stays resident, the cure is to stop the device and run the
|
|
||||||
same bring-up again, which `ServiceRecovery()` now does. It is rate-limited to
|
|
||||||
one attempt per five seconds and capped at three, because the reset has its own
|
|
||||||
multi-second handshakes and an adapter that will not come back after three
|
|
||||||
tries is genuinely broken - repeating it forever would be its own kind of
|
|
||||||
stall.
|
|
||||||
|
|
||||||
### The link is up only while the access point says so
|
|
||||||
|
|
||||||
`IwxLinkUp()` was nothing but `g_state == Connected`, and nothing moved it off
|
|
||||||
that state unless the access point was polite enough to send a
|
|
||||||
deauthentication frame. An access point that simply stops being there - a phone
|
|
||||||
hotspot that sleeps, wanders off channel, or drops the station without saying
|
|
||||||
so - left the link reported as up indefinitely. `NetIf::Active()` went on
|
|
||||||
choosing `wlan0`, every packet went into the void, and the desktop showed a
|
|
||||||
healthy connection while nothing resolved and nothing connected.
|
|
||||||
|
|
||||||
Beacon loss cannot be used to notice this: once associated, `MacConfigCmd`
|
|
||||||
stops asking for beacons and the firmware tracks them itself. What is
|
|
||||||
observable is that our own frames stop being acknowledged, since `IwxTxComplete`
|
|
||||||
gets a status per frame. A long enough run of failures - with any
|
|
||||||
acknowledgement, or any frame received from the BSS, restarting the count -
|
|
||||||
means the access point is gone, and the link comes down so the stack can fall
|
|
||||||
back to a cable and the panel can report the truth.
|
|
||||||
|
|
||||||
The threshold is counted in frames rather than seconds on purpose: a link that
|
|
||||||
is merely idle has nothing to send and must not be torn down for it. And
|
|
||||||
`IwxTxComplete` runs inside the RX pump, so it only sets a flag; the teardown
|
|
||||||
happens in `IwxConnectService()`, where sending the commands it needs is
|
|
||||||
allowed. `tests/wifi/ap_mlme.py` pins both halves of that.
|
|
||||||
|
|
||||||
## The desktop side
|
|
||||||
|
|
||||||
### Nothing in the GUI may block
|
|
||||||
|
|
||||||
`SYS_WIFI_SCAN` sweeps for up to twenty seconds and `SYS_WIFI_CONNECT` waits
|
|
||||||
out a whole handshake. Either one called from `desktop.elf` would freeze the
|
|
||||||
compositor - and with it the mouse, the panel and every window - for seconds at
|
|
||||||
a time. So the same work has a second, non-blocking entry point:
|
|
||||||
|
|
||||||
| | |
|
|
||||||
|---|---|
|
|
||||||
| `SYS_WIFI_SCAN_START` | starts a sweep and returns immediately |
|
|
||||||
| `SYS_WIFI_RESULTS` | copies the scan table out without touching the radio |
|
|
||||||
| `SYS_WIFI_CONNECT_ASYNC` | starts a join and returns immediately |
|
|
||||||
| `SYS_NETIFS` | lists the registered interfaces (see below) |
|
|
||||||
|
|
||||||
`WifiInfo` grew the fields that make polling enough to follow along:
|
|
||||||
`scanning` and `scanGeneration` for the sweep, `joining` and `connState` for
|
|
||||||
the handshake, and `lastError` for how the last join ended. The GUI reads
|
|
||||||
`SYS_WIFI_INFO` every 400 ms while something is in flight and every three
|
|
||||||
seconds otherwise.
|
|
||||||
|
|
||||||
The deadlines belong to the kernel, not the caller: `ServiceAsync()` runs from
|
|
||||||
`ServiceEvents()` - after the RX pump has returned, so it is allowed to send
|
|
||||||
commands - and aborts a scan that overruns, tears down a join that stalls, and
|
|
||||||
records `lastError` when one fails. A failed join is unwound the moment it
|
|
||||||
fails, so `connState` is back to idle by the time anyone looks; `lastError` is
|
|
||||||
what survives to be reported.
|
|
||||||
|
|
||||||
### Wired and wireless are separate on the panel
|
|
||||||
|
|
||||||
The IP configuration is global to the stack, so "which interface does this
|
|
||||||
address belong to?" is not a question `SYS_GETNETCFG` can answer. `SYS_NETIFS`
|
|
||||||
reports each registered interface with its name, MAC, link state and an
|
|
||||||
`active` flag - the one `NetIf::Active()` currently sends through. The Ethernet
|
|
||||||
popup shows the address only while the wired interface is the active one, and
|
|
||||||
says "Not in use" when the cable is up but Wi-Fi is carrying the traffic; the
|
|
||||||
Wi-Fi popup does the mirror image. Each icon appears only when its hardware
|
|
||||||
does: no wired interface, no Ethernet icon; no adapter, no Wi-Fi icon.
|
|
||||||
|
|
||||||
The Wi-Fi icon stays white whatever the radio is doing. State belongs in the
|
|
||||||
popup, and an icon that changes colour next to the clock is just noise.
|
|
||||||
|
|
||||||
Picking a network that needs a passphrase opens a real window - created with
|
|
||||||
`desktop_create_window()` and the four callbacks, exactly like the reboot and
|
|
||||||
shutdown dialogs in `dialogs.cpp` - rather than something painted into the
|
|
||||||
panel overlay. It therefore has a title bar, can be dragged and closed, appears
|
|
||||||
in the window list, and gets its text field, checkboxes and buttons from the
|
|
||||||
same `mtk` widgets the settings apps use.
|
|
||||||
|
|
||||||
### What runs at startup
|
|
||||||
|
|
||||||
`desktop.elf` keeps looking for an adapter until one appears (firmware loads
|
|
||||||
well after login), starts one scan as soon as the firmware reports ready, and
|
|
||||||
when the results land joins the strongest saved network that is in range. Once
|
|
||||||
the link is up, and only if no address is configured, it spawns `dhcp.elf`.
|
|
||||||
|
|
||||||
### Saved networks
|
|
||||||
|
|
||||||
`0:/config/wifi.toml` holds them:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[wifi]
|
|
||||||
autoconnect = true
|
|
||||||
|
|
||||||
[network.0]
|
|
||||||
ssid = "Home"
|
|
||||||
psk = "passphrase"
|
|
||||||
```
|
|
||||||
|
|
||||||
**No default copy of this file ships in the image.** It is created on the first
|
|
||||||
save, the way `bluetooth.toml`, `display.toml` and `session.toml` are. A shipped
|
|
||||||
default looks harmless - it only documents the schema - but it is laid down
|
|
||||||
again by anything that refreshes the system files, and it takes the user's saved
|
|
||||||
networks with it when it lands. That is exactly what happened the first time
|
|
||||||
this was written, and it is why the schema is documented here instead.
|
|
||||||
|
|
||||||
`programs/include/montauk/wifi.h` is the one implementation of reading,
|
|
||||||
writing and searching that file, shared by the panel, the Network app and the
|
|
||||||
`wifi` command, so all three agree on the schema. The passphrase is stored as
|
|
||||||
typed because that is what the join needs - the kernel derives the PMK from it,
|
|
||||||
or takes a 64-character hex string as a raw PSK. There is no key store to hide
|
|
||||||
it in: anyone who can read `0:/config` can read the passphrases.
|
|
||||||
|
|
||||||
## The interface registry
|
|
||||||
|
|
||||||
`Net::NetIf` replaced the Ethernet layer's direct calls into the E1000
|
|
||||||
drivers. Drivers register a name, a kind, and three function pointers; the
|
|
||||||
stack sends through `NetIf::Active()`, which prefers a wired interface with a
|
|
||||||
link and otherwise takes the first interface reporting one. A Wi-Fi-only
|
|
||||||
machine therefore has no link until it joins a network, and a machine with a
|
|
||||||
cable plugged in keeps using it.
|
|
||||||
|
|
||||||
`SYS_NETSTATUS` reports whichever interface is active, so `ifconfig` shows the
|
|
||||||
wireless counters once Wi-Fi is carrying traffic.
|
|
||||||
|
|
||||||
## What is supported
|
|
||||||
|
|
||||||
| | |
|
|
||||||
|---|---|
|
|
||||||
| Open networks | yes |
|
|
||||||
| WPA2-PSK, CCMP or GCMP | yes |
|
|
||||||
| WPA2-PSK-SHA256 | yes |
|
|
||||||
| WPA3 transition mode (PSK advertised alongside SAE) | yes, joins via PSK |
|
|
||||||
| Group key rekeying | yes |
|
|
||||||
| WPA3-only (SAE) | no |
|
|
||||||
| Management frame protection required (MFPR) | no |
|
|
||||||
| WEP, original WPA / TKIP | no |
|
|
||||||
| 802.1X enterprise (EAP) | no |
|
|
||||||
| Block-ack aggregation, HT/VHT/HE rates | no - legacy rates only |
|
|
||||||
|
|
||||||
SAE needs finite-field or elliptic-curve arithmetic that does not belong in
|
|
||||||
this kernel, and MFP needs BIP. Both are rejected up front with a specific
|
|
||||||
log line rather than failing partway through a handshake. Mixed WPA/WPA2
|
|
||||||
networks that still broadcast under TKIP are refused for the same reason: the
|
|
||||||
pairwise key would install but every broadcast frame would be dropped, which
|
|
||||||
looks like a working connection that cannot get a DHCP lease.
|
|
||||||
|
|
||||||
## Crypto
|
|
||||||
|
|
||||||
`kernel/src/Libraries/Crypto.cpp` exists because the supplicant runs in the
|
|
||||||
kernel and BearSSL is a userspace library. It provides SHA-1, SHA-256, HMAC
|
|
||||||
over both, PBKDF2-HMAC-SHA1, AES-128/256, RFC 3394 key wrap/unwrap and
|
|
||||||
AES-CMAC. It is not a general-purpose crypto library and should not be used
|
|
||||||
as one.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
`./tests/wifi/run.sh` compiles the shipping sources for the host against a
|
|
||||||
small shim and drives them from Python. It is the real `Crypto.cpp`,
|
|
||||||
`Wpa.cpp` and `IwxConnect.cpp`, not a copy, with only the transport stubbed.
|
|
||||||
|
|
||||||
- **Crypto primitives** against the published vectors - FIPS-197 for AES,
|
|
||||||
RFC 2202/4231 for HMAC, RFC 3394 for key wrap, RFC 4493 for CMAC, and the
|
|
||||||
IEEE 802.11i Annex H.4 WPA passphrase vectors for PBKDF2.
|
|
||||||
- **The supplicant** against an independent authenticator using `hashlib` and
|
|
||||||
`cryptography`: messages 2 and 4 carry MICs that verify under a PTK the AP
|
|
||||||
derived itself, the installed TK and GTK match the AP's, a wrong passphrase
|
|
||||||
produces a MIC the AP rejects, group rekeys and message-3 retransmissions
|
|
||||||
are answered, and RSN negotiation picks the right suites across eight
|
|
||||||
real-world information elements.
|
|
||||||
- **The MLME and data path** against a simulated AP that decodes every frame
|
|
||||||
the driver emits: the authentication request, the association request and
|
|
||||||
its elements (SSID, rates, capabilities, RSN), the handshake carried inside
|
|
||||||
real 802.11 data frames, key installation arguments, and the encapsulation
|
|
||||||
both ways - to-DS addressing, the protected bit, LLC/SNAP, sequence numbers,
|
|
||||||
broadcast delivery, and the filtering of foreign-BSSID and null-data frames.
|
|
||||||
Also the branches: open networks, retransmission and give-up when the AP is
|
|
||||||
silent, authentication and association rejections, and an AP-initiated
|
|
||||||
deauthentication bringing the link down.
|
|
||||||
- **Firmware context ordering** - the MLD command sizes and field offsets
|
|
||||||
against the decoded Linux trace, and the order the teardown unwinds the
|
|
||||||
contexts in, which is what the asserts above are about.
|
|
||||||
|
|
||||||
The harness clock advances on every host command
|
|
||||||
(`CMD_ROUND_TRIP_MS` in `mlme_harness.cpp`) rather than standing still. That
|
|
||||||
detail matters: a frozen clock makes every elapsed-time comparison in the
|
|
||||||
service loop trivially true or trivially false, and hid the underflow described
|
|
||||||
under "Sample the clock after the work, not before it" - the host tests passed
|
|
||||||
while the adapter could not join a network at all. Anything that reads
|
|
||||||
`Timekeeping::GetMilliseconds()` should be tested with time actually moving.
|
|
||||||
|
|
||||||
What is left needs the adapter, because it is the firmware's opinion rather
|
|
||||||
than the driver's logic: whether the firmware accepts the TX command and TFD
|
|
||||||
layout and actually radiates the frames, whether `ADD_STA_KEY` installs the
|
|
||||||
keys the driver asks for, whether the RX MPDU descriptor is read correctly off
|
|
||||||
real receptions, and whether association succeeds against a real AP's timing
|
|
||||||
and rate expectations. None of that can be exercised in QEMU, which has no
|
|
||||||
AX210-family device to emulate.
|
|
||||||
@@ -352,9 +352,7 @@ namespace montauk::abi {
|
|||||||
/* Storage.hpp */
|
/* Storage.hpp */
|
||||||
static constexpr uint64_t SYS_VOLINFO = 190; // (VolInfo*, maxCount) -> count, -1 bad pointer
|
static constexpr uint64_t SYS_VOLINFO = 190; // (VolInfo*, maxCount) -> count, -1 bad pointer
|
||||||
|
|
||||||
/* Consent.hpp -- first-use consent prompts (docs/consent.md). Only the
|
/* Consent.hpp */
|
||||||
session's consent agent (the desktop) may register, drain requests
|
|
||||||
and answer them. */
|
|
||||||
static constexpr uint64_t SYS_CONSENT_REGISTER = 191; // () -> 0, -13 lacks CAP_COMPOSITOR, -1 another agent is live
|
static constexpr uint64_t SYS_CONSENT_REGISTER = 191; // () -> 0, -13 lacks CAP_COMPOSITOR, -1 another agent is live
|
||||||
static constexpr uint64_t SYS_CONSENT_NEXT = 192; // (ConsentRequest*) -> 1 delivered, 0 none queued
|
static constexpr uint64_t SYS_CONSENT_NEXT = 192; // (ConsentRequest*) -> 1 delivered, 0 none queued
|
||||||
static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id
|
static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id
|
||||||
|
|||||||
@@ -265,9 +265,7 @@ namespace montauk::abi {
|
|||||||
/* Storage.hpp */
|
/* Storage.hpp */
|
||||||
static constexpr uint64_t SYS_VOLINFO = 190; // (VolInfo*, maxCount) -> count, -1 bad pointer
|
static constexpr uint64_t SYS_VOLINFO = 190; // (VolInfo*, maxCount) -> count, -1 bad pointer
|
||||||
|
|
||||||
/* Consent.hpp -- first-use consent prompts (docs/consent.md). Only the
|
/* Consent.hpp */
|
||||||
session's consent agent (the desktop) may register, drain requests
|
|
||||||
and answer them. */
|
|
||||||
static constexpr uint64_t SYS_CONSENT_REGISTER = 191; // () -> 0, -13 lacks CAP_COMPOSITOR, -1 another agent is live
|
static constexpr uint64_t SYS_CONSENT_REGISTER = 191; // () -> 0, -13 lacks CAP_COMPOSITOR, -1 another agent is live
|
||||||
static constexpr uint64_t SYS_CONSENT_NEXT = 192; // (ConsentRequest*) -> 1 delivered, 0 none queued
|
static constexpr uint64_t SYS_CONSENT_NEXT = 192; // (ConsentRequest*) -> 1 delivered, 0 none queued
|
||||||
static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id
|
static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* consent.h
|
* consent.h
|
||||||
* First-use consent: prompt wording and the stored-decision table
|
* First-use consent: prompt wording and the stored-decision table
|
||||||
* (0:/config/consent.toml). See docs/consent.md.
|
* (0:/config/consent.toml).
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ namespace montauk {
|
|||||||
return (int)syscall1(montauk::abi::SYS_GETCAPS, (uint64_t)out);
|
return (int)syscall1(montauk::abi::SYS_GETCAPS, (uint64_t)out);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consent agent (the desktop only; see docs/consent.md).
|
// Consent agent.
|
||||||
inline int consent_register() {
|
inline int consent_register() {
|
||||||
return (int)syscall0(montauk::abi::SYS_CONSENT_REGISTER);
|
return (int)syscall0(montauk::abi::SYS_CONSENT_REGISTER);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* consent.cpp
|
* consent.cpp
|
||||||
* Consent agent: answers the kernel's first-use capability requests from
|
* Desktpp consent agent
|
||||||
* stored decisions, or asks the user in a prompt window (docs/consent.md)
|
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -9,15 +8,6 @@
|
|||||||
#include <gui/mtk.hpp>
|
#include <gui/mtk.hpp>
|
||||||
#include <montauk/consent.h>
|
#include <montauk/consent.h>
|
||||||
|
|
||||||
// The prompt is an ordinary window inside the desktop, but an INTERNAL one:
|
|
||||||
// its pixels live in the desktop's heap, not in a window-server surface, so
|
|
||||||
// no app can map it (SYS_WINMAP) and only real input routed by the desktop
|
|
||||||
// reaches it. What remains is clickjacking -- an app raising a window over
|
|
||||||
// the prompt, or stealing focus, just as the user clicks. So Allow only
|
|
||||||
// arms after the prompt has been focused and on top for ARM_DELAY_MS without
|
|
||||||
// interruption; losing either restarts the delay. Don't Allow and closing
|
|
||||||
// are always safe and never wait.
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr int PROMPT_W = 460;
|
constexpr int PROMPT_W = 460;
|
||||||
|
|||||||
Reference in new issue
Block a user