v0.0.37 - Add interactive multi-transport selection menu at startup — users can select one or more transports (unix, qrexec bridge, TCP) from a menu instead of memorizing CLI flags

This commit is contained in:
Laan Tungir
2026-07-11 11:31:21 -04:00
parent 8ebdb50789
commit 2e8ce777d8
2 changed files with 379 additions and 16 deletions

View File

@@ -0,0 +1,143 @@
# Plan: Interactive multi-transport selection at startup
Status: design / ready for review.
Related:
- [`README.md`](../README.md) §7 Transport, §8.3 Qubes OS qube, §9 Usage
- [`src/main.c`](../src/main.c) — argument parsing, main loop, TUI
- [`src/server.c`](../src/server.c) — `server_ctx_t`, `server_start`, `server_handle_one`
- [`plans/qrexec_persistent_bridge.md`](qrexec_persistent_bridge.md) — bridge design
---
## 1. Motivation
The current CLI has many transport flags (`--listen`, `--socket-name`, `--bridge-source-trusted`, `--auth`, `--allow-all`) that the user must know in advance. For interactive use, this is unfriendly — the user just wants to answer "how should other programs reach this signer?"
Additionally, the user may want **multiple transports active simultaneously** (e.g. unix socket for local clients + TCP for FIPS mesh clients + qrexec bridge for other qubes). Today only one `--listen` mode is supported at a time.
---
## 2. Design
### 2.1 Interactive transport menu at startup
After mnemonic entry and before the running phase, if no `--listen` flag was given (i.e. interactive mode), present a multi-select menu:
```text
Transport — how should other programs reach this signer?
Select one or more (space to toggle, enter to confirm):
[x] 1. Local Unix socket (same machine/qube)
[ ] 2. Qubes qrexec bridge (other qubes via qrexec, no network)
[ ] 3. TCP listener (FIPS mesh or local network)
[ ] 4. Qrexec one-shot (legacy, one request per invocation)
[a] select all
(at least one must be selected)
```
Each selected transport gets configured with sensible defaults:
| Choice | What it starts | Defaults |
|---|---|---|
| 1. Unix socket | `--listen unix` | socket name `nsigner` (or random if collision) |
| 2. Qrexec bridge | `--listen unix --bridge-source-trusted` | socket name `nsigner`, accepts qrexec preamble |
| 3. TCP | `--listen tcp:[::]:8080` | bind all interfaces, port 8080 |
| 4. Qrexec one-shot | `--listen qrexec` | one request via stdin, then exit |
**Choices 1 and 2 can coexist** — they're both unix listeners, just with different identity handling. Choice 2 implies choice 1's socket. If both are selected, the bridge-source-trusted flag is set on the single unix listener (it handles both local and bridge connections).
**Choice 3 (TCP) can coexist with 1+2** — it's a separate listener on a different fd.
**Choice 4 (qrexec one-shot) is mutually exclusive** with all others — it uses stdin/stdout and exits after one request. If selected alone, it runs the one-shot path. If selected with others, it's ignored with a warning (or: the user is told it can't combine).
### 2.2 Multi-listener architecture
Currently `main()` creates one `server_ctx_t` and polls `server.listen_fd` + `STDIN_FILENO`. To support multiple listeners:
- Create an array of `server_ctx_t` (up to 3: unix, tcp, and optionally a second unix for bridge — though 1+2 collapse into one).
- Each `server_ctx_t` gets its own `server_start()`.
- The poll loop expands to poll all listener fds + STDIN_FILENO.
- When a listener fd has activity, call `server_handle_one()` on that context.
- The TUI status display shows all active transports.
```text
Connections
listen: unix @nsigner (bridge-source-trusted)
listen: tcp [::]:8080
client: nsigner --socket-name nsigner client '<json>'
```
### 2.3 Coexistence with CLI flags
- If `--listen` is given on the command line, **skip the interactive menu** (automation/scripting path unchanged).
- If no `--listen` is given and stdin is a TTY, **show the interactive menu**.
- If no `--listen` is given and stdin is NOT a TTY, **default to unix socket** (current behavior, for `--mnemonic-stdin` / `--mnemonic-fd` supervised launches).
This preserves backward compatibility: all existing flags and scripts work unchanged. The menu is purely an interactive convenience.
### 2.4 TUI implementation
The menu uses the existing TUI rendering helpers (`tui_render_content_screen`, `tui_print`). It appears after mnemonic acceptance and before the status display. Navigation:
- `1`-`4` or space: toggle a choice
- `a`: select all (except 4, which is mutually exclusive)
- `a`: select all (except 4, which is mutually exclusive)
- `enter`: confirm and proceed to running phase (at least one must be selected)
After confirmation, the selected listeners are started and the status display renders.
### 2.5 Security considerations
- The menu is purely a UX layer — it doesn't change the security model. Each transport still goes through the same policy/approval pipeline.
- `--bridge-source-trusted` is only set if the user explicitly selects the qrexec bridge option. It's never silently enabled.
- TCP listener still requires auth envelopes (existing behavior).
- The menu doesn't offer `--allow-all`; that remains a CLI flag for users who want it.
---
## 3. Implementation checklist
### 3.1 Multi-listener support in `main.c`
- [ ] Replace single `server_ctx_t server` with an array (e.g. `server_ctx_t servers[3]`).
- [ ] Add a `server_count` variable.
- [ ] Expand the poll loop to poll all `servers[i].listen_fd` + STDIN_FILENO.
- [ ] On activity, call `server_handle_one(&servers[i], ...)` for the matching fd.
- [ ] `server_stop()` all on shutdown.
### 3.2 Interactive menu
- [ ] Add `prompt_transport_selection()` function in `main.c` (after mnemonic load, before server start).
- [ ] Returns a bitmask of selected transports.
- [ ] Only called when `listen_mode` was not set by CLI and stdin is a TTY.
- [ ] Uses `tui_render_content_screen` + `read_line_stdin` for input.
### 3.3 Transport setup from menu selection
- [ ] Unix: `server_init` + `server_start` with `NSIGNER_LISTEN_UNIX`, socket name `nsigner`.
- [ ] Qrexec bridge: same as unix + `server_set_bridge_source_trusted(&server, 1)`.
- [ ] TCP: `server_init` + `server_start` with `NSIGNER_LISTEN_TCP`, target `tcp:[::]:8080`.
- [ ] Qrexec one-shot: existing `NSIGNER_LISTEN_QREXEC` path (stdin, one request, exit).
### 3.4 TUI status display
- [ ] Update `render_status()` to show multiple active listeners.
- [ ] Show `(bridge-source-trusted)` annotation on unix listeners that have it.
### 3.5 Testing
- [ ] Interactive: start nsigner with no `--listen`, select unix+TCP, verify both listeners work.
- [ ] Interactive: select qrexec bridge, verify bridge connections get `qubes:<vm>` identity.
- [ ] CLI: `--listen tcp:[::]:8080` still works (skips menu).
- [ ] CLI: `--listen unix --bridge-source-trusted` still works (skips menu).
- [ ] Non-TTY: `--mnemonic-stdin` without `--listen` defaults to unix (no menu).
---
## 4. Open questions
1. **Should the menu remember the last selection?** n_signer has no config files (by design). We could store the preference in an env var or a `/tmp` file, but that violates the zero-filesystem-footprint principle. **Decision: no persistence — the menu appears fresh each time.**
2. **Should there be a hotkey to add/remove transports at runtime?** E.g. press `t` in the TUI to bring up the transport menu again. This is a nice-to-have but adds complexity. **Decision: defer — the menu is startup-only for now.**
3. **TCP port selection in the menu?** The menu could ask for a port number if TCP is selected. **Decision: default to 8080, let the user override with `--listen tcp:...` if they need a different port. Keep the menu simple.**
4. **Socket name selection?** Same — default to `nsigner`, override with `--socket-name` if needed. **Decision: default, no prompt.**