texas2step 2.2.4__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. {texas2step-2.2.4 → texas2step-2.3.0}/PKG-INFO +1 -1
  2. {texas2step-2.2.4 → texas2step-2.3.0}/docs/access.md +114 -7
  3. {texas2step-2.2.4 → texas2step-2.3.0}/docs/api.md +5 -4
  4. {texas2step-2.2.4 → texas2step-2.3.0}/docs/configuration.md +4 -2
  5. {texas2step-2.2.4 → texas2step-2.3.0}/docs/install.md +35 -8
  6. {texas2step-2.2.4 → texas2step-2.3.0}/docs/methodology.md +8 -1
  7. {texas2step-2.2.4 → texas2step-2.3.0}/docs/roadmap.md +13 -0
  8. {texas2step-2.2.4 → texas2step-2.3.0}/docs/security.md +10 -4
  9. {texas2step-2.2.4 → texas2step-2.3.0}/docs/traffic.md +3 -1
  10. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/__init__.py +1 -1
  11. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/access.py +47 -20
  12. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/api/access.py +59 -15
  13. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/api/meta.py +6 -2
  14. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/api/wire.py +11 -5
  15. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/app.py +8 -5
  16. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/cli.py +188 -13
  17. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/cluster.py +18 -5
  18. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/config.py +14 -4
  19. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/config_template.toml +4 -1
  20. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/lockouts.py +86 -8
  21. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/rtc.py +19 -5
  22. texas2step-2.3.0/src/texas2step/static/assets/app.css +1 -0
  23. texas2step-2.3.0/src/texas2step/static/assets/app.js +8 -0
  24. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/static/index.html +2 -2
  25. texas2step-2.2.4/src/texas2step/static/assets/app.css +0 -1
  26. texas2step-2.2.4/src/texas2step/static/assets/app.js +0 -8
  27. {texas2step-2.2.4 → texas2step-2.3.0}/.gitignore +0 -0
  28. {texas2step-2.2.4 → texas2step-2.3.0}/LICENSE.md +0 -0
  29. {texas2step-2.2.4 → texas2step-2.3.0}/README.md +0 -0
  30. {texas2step-2.2.4 → texas2step-2.3.0}/deploy/nginx/texas2step-http.conf +0 -0
  31. {texas2step-2.2.4 → texas2step-2.3.0}/deploy/nginx/texas2step-log.conf +0 -0
  32. {texas2step-2.2.4 → texas2step-2.3.0}/deploy/nginx/texas2step-speedtest.conf +0 -0
  33. {texas2step-2.2.4 → texas2step-2.3.0}/deploy/nginx/texas2step.conf +0 -0
  34. {texas2step-2.2.4 → texas2step-2.3.0}/deploy/texas2step-update.service +0 -0
  35. {texas2step-2.2.4 → texas2step-2.3.0}/deploy/texas2step-update.timer +0 -0
  36. {texas2step-2.2.4 → texas2step-2.3.0}/deploy/texas2step.default +0 -0
  37. {texas2step-2.2.4 → texas2step-2.3.0}/deploy/texas2step.service +0 -0
  38. {texas2step-2.2.4 → texas2step-2.3.0}/docs/development.md +0 -0
  39. {texas2step-2.2.4 → texas2step-2.3.0}/docs/experience.md +0 -0
  40. {texas2step-2.2.4 → texas2step-2.3.0}/docs/images/hero-dark.jpg +0 -0
  41. {texas2step-2.2.4 → texas2step-2.3.0}/docs/images/results-dark.jpg +0 -0
  42. {texas2step-2.2.4 → texas2step-2.3.0}/docs/images/results-light.jpg +0 -0
  43. {texas2step-2.2.4 → texas2step-2.3.0}/docs/images/upload-light.jpg +0 -0
  44. {texas2step-2.2.4 → texas2step-2.3.0}/legacy/README.md +0 -0
  45. {texas2step-2.2.4 → texas2step-2.3.0}/pyproject.toml +0 -0
  46. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/__main__.py +0 -0
  47. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/api/__init__.py +0 -0
  48. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/api/measure.py +0 -0
  49. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/api/results.py +0 -0
  50. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/config_edit.py +0 -0
  51. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/config_merge.py +0 -0
  52. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/engine.py +0 -0
  53. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/iface.py +0 -0
  54. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/logs.py +0 -0
  55. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/page.py +0 -0
  56. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/payload.py +0 -0
  57. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/quality.py +0 -0
  58. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/static/assets/swagger/LICENSE +0 -0
  59. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/static/assets/swagger/NOTICE +0 -0
  60. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/static/assets/swagger/swagger-ui-bundle.js +0 -0
  61. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/static/assets/swagger/swagger-ui-bundle.js.LICENSE.txt +0 -0
  62. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/static/assets/swagger/swagger-ui.css +0 -0
  63. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/static/favicon.svg +0 -0
  64. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/stats.py +0 -0
  65. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/store.py +0 -0
  66. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/traffic.py +0 -0
  67. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/update.py +0 -0
  68. {texas2step-2.2.4 → texas2step-2.3.0}/src/texas2step/words.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: texas2step
3
- Version: 2.2.4
3
+ Version: 2.3.0
4
4
  Summary: Web-centric throughput, latency, jitter and packet-loss measurement between endpoints.
5
5
  Project-URL: Homepage, https://github.com/robert-patrick-texas/texas2step
6
6
  Project-URL: Documentation, https://robert-patrick-texas.github.io/texas2step/
@@ -18,7 +18,8 @@ no user names or passwords. The desk's own tools stay on your networks: saved re
18
18
  works out the same code from it, with nothing shared between them while they run.
19
19
  * **A code buys a pass.** The visitor enters the code once. The server answers with a
20
20
  pass, good for 12 hours (`access_pass_hours`), which the browser then sends with every
21
- request, to every server in the picker. A server with the same key accepts it.
21
+ request, to every server in the picker. A server with the same key accepts it. To end
22
+ every pass at once, see [Ending every pass](#ending-every-pass).
22
23
  * **The desk's tools stay inside.** Visitors from addresses in `internal_networks` need no
23
24
  code, and only they get History, the API tab, saved results and the code itself.
24
25
  Everyone else gets the Test, Monitor and Diag tabs.
@@ -88,7 +89,8 @@ To turn codes off again: `sudo texas2step config set access_code=off --write`, t
88
89
  | `access_timezone` | `America/New_York` | The time zone whose midnight starts a new code; the same on every server |
89
90
  | `access_words` | `""` | Your own word list; empty uses the built-in words |
90
91
  | `access_digits` | `2` | The digits that end a code: 2, 3 or 4 |
91
- | `access_pass_hours` | `12` | How long an entered code lets a browser keep testing |
92
+ | `access_pass_hours` | `12` | How long a pass lasts from when it was issued (1 to 168). Each server applies its own as passes are used, so a change applies to passes already given; use the same on every server. |
93
+ | `access_note` | `Contact the service desk for assistance.` | The line under the code box, such as how to reach the desk; `""` for none |
92
94
  | `access_key_file` | `/etc/texas2step/access.key` | The key |
93
95
  | `access_phrases_file` | `/etc/texas2step/access-phrases.toml` | [Standing phrases](#standing-phrases), if the file exists |
94
96
  | `internal_networks` | `[]` | Your own networks: no code, and the desk's tools. A list of IPv4 and IPv6 addresses and networks in CIDR form, mixed freely: `["192.168.0.0/16", "2001:db8::/32", "203.0.113.7"]`. An address alone is a single host. Bits past the prefix are ignored, so `10.2.1.30/24` means all of `10.2.1.0/24`. Give the addresses as the server sees them: staff who come in through your firewall's NAT arrive from its public address. |
@@ -112,8 +114,9 @@ tests until it changes, but for a short list, or a `fixed` code, consider
112
114
  `access_digits = 3` or `4`.
113
115
 
114
116
  **When it changes.** A new code starts at midnight in `access_timezone`. The previous
115
- code keeps working for 24 hours after the change, for a call that spans midnight. A
116
- `fixed` code changes only with a new key:
117
+ code keeps working for 24 hours after the change, for a call that spans midnight. To
118
+ change it before then, or to change a `fixed` code, [shuffle the codes](#shuffling-the-codes),
119
+ or make a new key:
117
120
 
118
121
  ```bash
119
122
  sudo texas2step access key --new
@@ -123,6 +126,42 @@ That makes a new key at once: every code and pass from the old one stops working
123
126
  the new key to the other servers as in step 1, and restart the service on each. Without
124
127
  `--yes`, it asks first.
125
128
 
129
+ ### Shuffling the codes
130
+
131
+ When a code has gone further than it should, shuffle the codes. The current code and the
132
+ previous one stop working at once, and a new one takes their place:
133
+
134
+ ```console
135
+ $ sudo texas2step access shuffle
136
+ Shuffle the codes? The current and previous codes stop working, and the desk reads the new one. [y/N] y
137
+ Codes shuffled here. Today's code is now copper.lantern.42; the codes before it don't work.
138
+ Passes already given keep working: add --end-passes to end them as well.
139
+ Other servers: sent to dallas, houston.
140
+ ```
141
+
142
+ A shuffle is a random value mixed into every code from then on. It is kept in
143
+ `access_lockouts_file`, so it needs no new key and no restart, and outlasts a restart.
144
+ Every code entered after it is checked against it, on every worker. The desk reads the
145
+ new code at the top of History, or with `sudo texas2step access show`, which also says
146
+ when the codes were shuffled. There is no previous code until the next change: at
147
+ midnight, the shuffled code becomes yesterday's and works for 24 hours as usual. Phrases
148
+ are not codes, so they keep working.
149
+
150
+ Every server must hold the same shuffle, or they give different codes. With
151
+ [servers that defend together](#servers-that-defend-together), it goes to every server in
152
+ `[[locations]]`, and each keeps the latest it is given. Without them, the command prints
153
+ the value for the others; run that on each:
154
+
155
+ ```bash
156
+ sudo texas2step access shuffle --to 3f9a0c7e12b4d856
157
+ ```
158
+
159
+ `access show`'s fingerprint includes the shuffle, so servers that agree show the same one.
160
+
161
+ Passes already given keep working. Add `--end-passes` to [end them](#ending-every-pass)
162
+ too, which has everyone outside enter the new code: the same as a new key, without
163
+ copying a key to every server.
164
+
126
165
  **Typing it.** Case and separators don't matter: `Maple River 42`, `maple-river-42` and
127
166
  `maple.river.42` are the same code, and `7` counts for `07`.
128
167
 
@@ -204,7 +243,8 @@ journalctl -u texas2step --grep 'access phrase'
204
243
  stays guessable: several words that don't belong together, such as
205
244
  `copper lantern harbor violet`, rather than `vip access`. texas2step takes any phrase
206
245
  you choose. To withdraw one, `sudo texas2step access phrase remove vip` on every server,
207
- and restart; passes it already bought last until they expire (`access_pass_hours`).
246
+ and restart. Passes it already bought last until they run out (`access_pass_hours`),
247
+ unless you [end every pass](#ending-every-pass).
208
248
  A new key doesn't change the phrases.
209
249
 
210
250
  A phrase buys the same pass as the day's code, so the visitor gets what any visitor
@@ -273,6 +313,30 @@ inside.
273
313
  Without `internal_networks`, every visitor needs a code, and anyone with one can read the
274
314
  saved results. `texas2step access show` warns about that.
275
315
 
316
+ ### Inside on one address family
317
+
318
+ A visitor with both IPv4 and IPv6 comes from two addresses, and `internal_networks` may
319
+ list only one of them: an office's IPv6 network, say, but not the NAT address its IPv4
320
+ leaves by. The page asks its own server, at the address it was opened at, whether the
321
+ code is needed, and the browser reaches that over either family. When the answer is no
322
+ but another address the page tests refuses it, the page asks for the code over itself,
323
+ in a box the visitor can put aside with **Not now**. That other address is this server's
324
+ IPv4-only or IPv6-only name ([IPv4 and IPv6](configuration.md#ipv4-and-ipv6)), or another
325
+ server in the picker. The pass the code buys works over both families and on every
326
+ server with the same key.
327
+
328
+ The box comes up by itself until the visitor puts it aside, which holds until the page
329
+ is loaded again. Meanwhile Test and Monitor say the function is restricted over that
330
+ address, with a button to enter the code, and Diag's rows for it say **Restricted**, with
331
+ the same button.
332
+ Diag then tries that address every 10 seconds instead of every second, and opens no
333
+ WebSocket there until the code is in. A server with `access_code = "off"` has no pass to
334
+ give, so its page doesn't ask: it only says which address wants a code.
335
+
336
+ A pass that hasn't run out but is refused anyway, as from a network blocked by hand
337
+ ([lockouts and blocks](#lockouts-and-blocks)), isn't asked for again by itself. To have
338
+ neither family asked, list both of a site's addresses in `internal_networks`.
339
+
276
340
  ## Lockouts and blocks
277
341
 
278
342
  Wrong codes count against the visitor's network, its IPv4 /24 or IPv6 /48, not its
@@ -377,6 +441,44 @@ every 30 minutes. One with 10,000 networks gets about 1.4 million a day:
377
441
  For a server open to the Internet, use `access_digits = 4`, and standing phrases of
378
442
  several unrelated words.
379
443
 
444
+ ## Ending every pass
445
+
446
+ A pass holds the time it was issued, signed with the key: `1793030400.q7GxN0x2rY8vLmKp3TfWcA9b`.
447
+ No server keeps a list of them. Each server checks a pass as it is used:
448
+
449
+ * The signature matches, so a server with the same key issued it.
450
+ * It was issued less than `access_pass_hours` ago, by this server's setting now. Lower
451
+ the setting, and restart, and passes already given end sooner; raise it, and they last
452
+ longer.
453
+ * It wasn't issued more than 5 minutes ahead of this server's clock.
454
+ * It was issued after passes were last ended.
455
+
456
+ To end every pass given so far, such as when a code or a phrase went further than it
457
+ should have:
458
+
459
+ ```console
460
+ $ sudo texas2step access passes --end
461
+ End every pass issued so far? Everyone testing from outside enters the code again. [y/N] y
462
+ Every pass issued before 2026-10-25 14:02 EDT is ended here, within seconds; tests running with one stop too.
463
+ Other servers: sent to dallas, houston.
464
+ ```
465
+
466
+ The time is kept in `access_lockouts_file`, beside the lockouts, so it needs no restart
467
+ and outlasts one. Each of the service's workers reads it within 5 seconds. From then on,
468
+ a pass issued before it is refused as if there were none (`401`), and the WebSockets and
469
+ UDP sessions opened with one close within a few seconds. The page asks for the code
470
+ again. The codes and phrases still work, so everyone with the current code is back in at
471
+ once, with a new pass. Visitors from `internal_networks` need no pass, so nothing changes
472
+ for them.
473
+
474
+ With [servers that defend together](#servers-that-defend-together), the end goes to every
475
+ server in `[[locations]]`, and each keeps the later of its own and the one it is sent.
476
+ Without them, run the command on each server: until then, the others still take the
477
+ passes. `sudo texas2step access passes` says how long passes last here and when they were
478
+ last ended, and `access show` does too. To end the codes too, use
479
+ `sudo texas2step access shuffle --end-passes` ([Shuffling the codes](#shuffling-the-codes)).
480
+ A new key also ends every pass, and every code with it.
481
+
380
482
  ## Servers that defend together
381
483
 
382
484
  When a lockout begins on one server, the others can block that network too, so an
@@ -391,8 +493,9 @@ access_peer_networks = ["198.51.100.20", "2001:db8:20::20", "203.0.113.30"]
391
493
  * **What goes to the others.** A lockout here goes, as a block, to every server in
392
494
  `[[locations]]`, at once. While the attacker keeps trying here, it goes again every
393
495
  third of `access_lockout_minutes`, so the others' blocks don't run out first.
394
- `sudo texas2step access block` and `access lockouts --clear` go to every server too:
395
- `--clear` on any one of them lets a caller back in everywhere. The
496
+ `sudo texas2step access block`, `access lockouts --clear`, `access passes --end` and
497
+ `access shuffle` go to every server too: `--clear` on any one of them lets a caller
498
+ back in everywhere. The
396
499
  [API's blocks](#lockouts-and-blocks) stay on the server they are sent to. A block from
397
500
  `access block` ends passes on every server, as here; a lockout ends none anywhere. A
398
501
  server on an earlier version sends either as a lockout.
@@ -407,6 +510,10 @@ access_peer_networks = ["198.51.100.20", "2001:db8:20::20", "203.0.113.30"]
407
510
  compromised: a block from another server covers at most a /24 or a /48, never
408
511
  `internal_networks` or loopback, lasts at most a week, and isn't passed on. A server
409
512
  holds at most 1,000 of them. A wider block by hand stays on the server where it was set.
513
+ Another server can end every pass issued before its request, which at worst has
514
+ everyone enter the code again, but not set that time later than the receiving server's
515
+ clock allows, which would stamp new passes there too far ahead. It can shuffle the
516
+ codes, which at worst has the desk read the new one.
410
517
  * **Counts stay on each server.** Wrong codes count where they are entered, so with three
411
518
  servers an attacker gets up to about seven guesses, two at each and one more, before
412
519
  the whole cluster locks them out.
@@ -78,7 +78,8 @@ sending the offer. The offer must include data channels:
78
78
  profiles the session runs. A voice or video session counts toward a
79
79
  [traffic limit](traffic.md#monitors-call-profiles). The answer is
80
80
  `{"op":"profile","ok":true,"name":"voice"}`, or `"ok": false` with a `reason`: the
81
- server withholds video (`monitor_video = false`), or a traffic limit is reached, when
81
+ server withholds that profile (`monitor_voice` or `monitor_video` is `false`, as
82
+ `/api/v1/info`'s `monitor` says), or a traffic limit is reached, when
82
83
  the site's `message` and `url` come with it. Past a limit while a session runs, the
83
84
  server sends `{"op":"limit","message":…,"url":…}` unasked and from then on echoes only
84
85
  probes of up to 64 bytes.
@@ -91,7 +92,7 @@ every port in its `rtc_port_min`–`rtc_port_max` range is taken.
91
92
 
92
93
  | Method | Path | Returns |
93
94
  |---|---|---|
94
- | GET | `/api/v1/info` | name, version (left out for clients outside `internal_networks`), server identity, capabilities (`stamps`: the UDP probe can [stamp](#udp-probe-protocol) its echoes), limits, ICE servers, UI defaults (`one_stream`: whether a test downloads over one stream too, unless asked otherwise), `monitor` (`profile`: Monitor's until a visitor picks; `video`: whether the Video call profile is offered); with a [traffic limit](traffic.md), `traffic` (below) |
95
+ | GET | `/api/v1/info` | name, version (left out for clients outside `internal_networks`), server identity, capabilities (`stamps`: the UDP probe can [stamp](#udp-probe-protocol) its echoes), limits, ICE servers, UI defaults (`one_stream`: whether a test downloads over one stream too, unless asked otherwise), `monitor` (`profile`: Monitor's until a visitor picks; `voice`, `video`: whether the Voice call and Video call profiles are offered); with a [traffic limit](traffic.md), `traffic` (below) |
95
96
  | GET | `/api/v1/whoami` | client IP, port, family, scheme, HTTP version, user agent |
96
97
  | GET | `/api/v1/diag/http` | both ends of this HTTP connection, its HTTP version and TLS, the server's TCP RTT and clock (below) |
97
98
  | GET | `/api/v1/locations` | servers offered in the picker (this one first) |
@@ -187,13 +188,13 @@ clients that would need a pass.
187
188
 
188
189
  | Method | Path | Purpose |
189
190
  |---|---|---|
190
- | GET | `/api/v1/access?pass=<pass>` | `{"required", "desk"}`: whether this client needs a pass, and gets the desk's functions; with `pass`, also whether that pass is valid and when it expires |
191
+ | GET | `/api/v1/access?pass=<pass>` | `{"required", "desk", "codes", "note"}`: whether this client needs a pass, gets the desk's functions, and whether this server gives passes at all (`access_code` is on), and the site's line under the code box (`access_note`, empty for none); with `pass`, also whether that pass is valid and when it expires |
191
192
  | POST | `/api/v1/access` | `{"code": "maple.river.42"}` (any case and separators, or a standing phrase) returns `{"pass", "expires"}`. A wrong code is `403`. After `access_lockout_after` wrong codes in a row from one /24 or /48, every code from it gets that same `403`, unread, until `access_lockout_minutes` pass without one ([Lockouts and blocks](access.md#lockouts-and-blocks)). A body over 1 KB is `413` |
192
193
  | GET | `/api/v1/access/code` | The current code, when it changes, and the previous one while it still works; only from `internal_networks` |
193
194
  | GET | `/api/v1/access/blocks` | `{"lockouts": [{"network", "source", "wrong_codes", "since", "until", "by_hand"}], "counting"}` (`by_hand`: a block, which ends the passes used from the network too); only from `internal_networks` and the server itself |
194
195
  | POST | `/api/v1/access/blocks` | `{"network": "198.51.100.0/24", "minutes": 120}` locks a network out by hand, or keeps its lockout going, and ends the passes used from it; `minutes` defaults to `access_lockout_minutes`. One that overlaps `internal_networks`, or is wider than a /16 or a /32, is `400` |
195
196
  | DELETE | `/api/v1/access/blocks?network=<network>` | Ends the lockouts that overlap it (`all`: every one); returns `{"cleared": [...]}` |
196
- | POST | `/api/v1/access/peer` | For [servers that defend together](access.md#servers-that-defend-together): `{"from", "to", "action": "block" \| "clear" \| "check", "network", "minutes", "kind": "lockout" \| "block"}` (`kind`: a block set by hand, which ends passes there too; `lockout` when left out), signed in `X-Texas2step-Time`, `-Nonce` and `-Signature`. Only from `access_peer_networks`, with `access_share_lockouts` on; `404` to everyone else |
197
+ | POST | `/api/v1/access/peer` | For [servers that defend together](access.md#servers-that-defend-together): `{"from", "to", "action": "block" \| "clear" \| "check" \| "end-passes" \| "shuffle", "network", "minutes", "kind": "lockout" \| "block", "at", "value"}` (`kind`: a block set by hand, which ends passes there too; `lockout` when left out. `end-passes` ends every pass issued before `at`, in Unix seconds; `shuffle` mixes `value`, 16 hex digits, into the codes from `at`, the latest shuffle staying; neither `at` may be later than the receiving server's clock allows), signed in `X-Texas2step-Time`, `-Nonce` and `-Signature`. Only from `access_peer_networks`, with `access_share_lockouts` on; `404` to everyone else |
197
198
 
198
199
  ```bash
199
200
  pass=$(curl -s -X POST https://speedtest.example.net/texas2step/api/v1/access \
@@ -38,7 +38,8 @@ file up to date with the new version's template and keeps your values; see
38
38
  | `page_test` / `page_monitor` / `page_diag` / `page_history` / `page_api` | `true` | Show that tab of the web UI. Any can be hidden, but not all. A hidden page's address opens the first page shown. The API stays on: `results_enabled` and `measure_enabled` turn parts of it off, and `/api/docs` stays reachable without the API tab. |
39
39
  | `default_streams` / `default_seconds` / `default_packets` | 4 / 10 / 500 | Defaults advertised by `/api/v1/info` |
40
40
  | `monitor_profile` | `path` | Monitor's profile until a visitor picks one: `path` (small probes, to follow the path), `voice` or `video` (a call's packets, scored every 10 seconds); see [Live monitor](methodology.md#live-monitor) |
41
- | `monitor_video` | `true` | Offer Monitor's Video call profile, about 1.4 Mb/s each way while a tab runs it. Voice and video count toward a [traffic limit](traffic.md#monitors-call-profiles). |
41
+ | `monitor_voice` | `true` | Offer Monitor's Voice call profile, about 150 kb/s each way while a tab runs it. With `false`, the page shows no Voice call button and the server refuses the profile. |
42
+ | `monitor_video` | `true` | Offer Monitor's Video call profile, about 1.4 Mb/s each way while a tab runs it. With `false`, the page shows no Video call button and the server refuses the profile. With both `false`, Monitor runs Path only, and shows no profile switch. Voice and video count toward a [traffic limit](traffic.md#monitors-call-profiles). |
42
43
  | `default_one_stream` | `true` | Whether a test also downloads over [one stream](methodology.md#one-stream), unless the visitor chooses in Test options or the command line with `--one-stream` or `--no-one-stream`. It can add up to half again to a test's traffic, so a server whose traffic is billed may want `false`; see [Traffic limits](traffic.md#what-a-test-moves). |
43
44
  | `cors_origins` | `["*"]` | Origins allowed to call the API from a browser |
44
45
  | `trusted_proxies` | `["127.0.0.1", "::1"]` | Peers whose `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Prefix` and `X-Client-Protocol` are believed |
@@ -48,7 +49,8 @@ file up to date with the new version's template and keeps your values; see
48
49
  | `access_timezone` | `America/New_York` | The time zone whose midnight starts a new code |
49
50
  | `access_words` | `""` | A file of your own words for codes, one entry per line; empty uses the built-in words |
50
51
  | `access_digits` | 2 | The digits that end a code: 2, 3 or 4 |
51
- | `access_pass_hours` | 12 | How long an entered code lets a browser keep testing |
52
+ | `access_pass_hours` | 12 | How long a pass lasts from when it was issued; applied as passes are used, so a change applies to passes already given. See [Ending every pass](access.md#ending-every-pass). |
53
+ | `access_note` | `Contact the service desk for assistance.` | The line under the code box, such as how to reach the desk; `""` for none |
52
54
  | `access_key_file` | `/etc/texas2step/access.key` | The key codes come from; the same on every server of an organization |
53
55
  | `access_phrases_file` | `/etc/texas2step/access-phrases.toml` | Standing phrases that work as codes, if the file exists; see [Standing phrases](access.md#standing-phrases) |
54
56
  | `access_lockout_after` | 3 | Wrong codes in a row from one /24 or /48 that lock it out of entering codes; see [Lockouts and blocks](access.md#lockouts-and-blocks) |
@@ -51,7 +51,8 @@ To do the same by hand, follow steps 1 to 5 below instead.
51
51
  ```
52
52
 
53
53
  It logs to `texas2step-install.log` beside itself. When it finishes, the service
54
- answers both on `127.0.0.1:8080` and through nginx, and it prints the address to open.
54
+ answers both on `127.0.0.1:8080` (or the `--port` you gave) and through nginx, and it
55
+ prints the address to open.
55
56
  You can run it again safely: it skips each step that is already done.
56
57
 
57
58
  What the check looks at, and what the script then does:
@@ -66,7 +67,7 @@ What the check looks at, and what the script then does:
66
67
  | Peers | Each `--peer URL` is asked who it is (`/api/v1/info`). A peer that can't be reached is left out, with the command to add it later. | `texas2step config add-location ... --write` |
67
68
  | nginx | nginx is installed and running, its configuration passes `nginx -t`, and it loads `conf.d/*.conf`. It finds your HTTPS site, and checks that nothing else on that site uses `/texas2step/`. If texas2step is already published, nginx is left alone. | Adds `include snippets/texas2step.conf;` after the site's `server_name` line, runs `nginx -t` and reloads. The file is backed up to `/var/backups/texas2step/` first, and put back if the test fails. |
68
69
  | UDP | The probe's port range: the one you set, or 61000–61999. It checks this range against the firewall and the kernel's ephemeral ports. | Pins the range in the settings. Opens it in ufw or firewalld if one of them is active. |
69
- | Port 8080 | Nothing else listens there | |
70
+ | Port | Nothing else listens on texas2step's TCP port: 8080, `LISTEN_PORT` in `/etc/default/texas2step`, or `--port`. nginx's `upstream` in `/etc/nginx/conf.d/texas2step.conf` points at that port. | With `--port`, sets `LISTEN_PORT` and the `upstream` before the package is installed, so the service never starts on 8080. An `upstream` that points elsewhere is pointed at the port. |
70
71
 
71
72
  Options:
72
73
 
@@ -79,6 +80,7 @@ Options:
79
80
  | `--path PATH` | Publish texas2step at `PATH` instead of `/texas2step/`, e.g. `/speedtest`. The check moves the package's snippet to that path and shows it in the install script, which writes it as `snippets/texas2step-at-speedtest.conf` and includes that. Each later run writes it again from the new package. Use the same path in `--url-v4` and `--url-v6`. |
80
81
  | `--no-nginx` | Install the service only, and publish it yourself |
81
82
  | `--udp-ports MIN-MAX` | The probe's UDP range, e.g. `61000-61999` |
83
+ | `--port PORT` | The TCP port texas2step listens on, on `127.0.0.1` behind nginx, when another program needs 8080. Choose one from 1024 to 32767. Later runs and `texas2step update` keep it. See [Another port](#another-port). |
82
84
  | `--url-v4 URL`, `--url-v6 URL` | This server's IPv4-only and IPv6-only base URLs, for the header's IP drop-down; see [IPv4 and IPv6](configuration.md#ipv4-and-ipv6). The check looks up each name: it should have only an A record (or only AAAA), pointing at this server. After the install, the script checks that nginx answers for each name. |
83
85
  | `--no-firewall` | Leave the firewall alone; the report lists the rule to add |
84
86
  | `--allow-downgrade` | Allow installing an older version than the one installed |
@@ -128,6 +130,32 @@ yourself.
128
130
  To make the second server by cloning the first's virtual machine instead, see
129
131
  [Clone a server](#clone-a-server).
130
132
 
133
+ ### Another port
134
+
135
+ texas2step listens on `127.0.0.1:8080`, and nginx reaches it there through the
136
+ `upstream` in `/etc/nginx/conf.d/texas2step.conf`. When another program needs 8080,
137
+ give the setup tool another port, the same way for a new install or an installed server:
138
+
139
+ ```bash
140
+ sudo ./texas2step-setup --port 8081
141
+ sudo ./texas2step-install.sh
142
+ ```
143
+
144
+ The install script sets `LISTEN_PORT=8081` in `/etc/default/texas2step` and points the
145
+ `upstream` at `127.0.0.1:8081`, keeping a dated copy of each file it changes in
146
+ `/var/backups/texas2step/`. It does this before installing the package, so the service
147
+ never starts on 8080, even on a new install. Then it restarts texas2step, reloads nginx
148
+ and checks both.
149
+
150
+ Choose a port from 1024 to 32767. texas2step runs as its own user, which can't listen
151
+ below 1024, and Linux takes the ports of outgoing connections from 32768 up. If the port
152
+ is in use, the check says by what. Both files are kept on upgrades, and the setup tool
153
+ and `texas2step update` read the port from `LISTEN_PORT`. When a new version changes the
154
+ `upstream` file, the update installs the new one with your port in it.
155
+
156
+ If you changed `LISTEN_PORT` by hand and not the `upstream`, the next run of the setup
157
+ tool points the `upstream` at it.
158
+
131
159
  ## 1. Install the package
132
160
 
133
161
  Download the newest `.deb` from the
@@ -264,12 +292,11 @@ choose the default theme (Light, Dark, America, Energy, Federal, Military or Tex
264
292
  that site directly. Sites can also measure each other with `POST /api/v1/measure/full`,
265
293
  but only toward listed locations.
266
294
 
267
- **Listen address and workers.** These are set in `/etc/default/texas2step`. If you move
268
- the service off `127.0.0.1:8080`, change the `upstream` in
269
- `/etc/nginx/conf.d/texas2step.conf` to match. More workers help on multi-gigabit links,
270
- where Python's per-connection CPU cost is the limit. Each worker keeps its own UDP
271
- sessions, which is fine, because a session lives entirely in the worker that answered
272
- it.
295
+ **Listen address and workers.** These are set in `/etc/default/texas2step`. To move the
296
+ service to another port, see [Another port](#another-port). More workers help on
297
+ multi-gigabit links, where Python's per-connection CPU cost is the limit. Each worker
298
+ keeps its own UDP sessions, which is fine, because a session lives entirely in the
299
+ worker that answered it.
273
300
 
274
301
  ## 4. Firewall and UDP
275
302
 
@@ -272,7 +272,10 @@ counted as lost. Statistics cover the visible window (1, 5 or 15 minutes).
272
272
  A **profile** sets what is sent. Anyone can switch it beside the Start button. Options
273
273
  holds the exact sizes and rates, and the page's address carries them, so the service
274
274
  desk can send a caller a link such as `/monitor?profile=voice`. The site picks the
275
- profile a visitor starts with (`monitor_profile`).
275
+ profile a visitor starts with (`monitor_profile`), and can withhold the call profiles
276
+ (`monitor_voice`, `monitor_video`): the switch then shows only the ones offered, and none
277
+ at all when only Path is. A link or a saved choice for a withheld profile starts with
278
+ the nearest one offered.
276
279
 
277
280
  | Profile | Sends | Each way, as the network carries it |
278
281
  |---|---|---|
@@ -365,6 +368,10 @@ and closes its connections. Its rows and events stay, and the timelines show the
365
368
  An attempt not answered within 2 s (1.5 s for WebSocket and UDP) counts as missed. A
366
369
  method that has answered before shows a miss as loss (red). One that hasn't answered
367
370
  yet stays grey and keeps trying. One the server doesn't offer is grey and isn't tried.
371
+ One whose address asks for the access code, as a server's IPv4-only name can for a
372
+ visitor inside on IPv6 only, says **Restricted** (amber) and doesn't count as loss:
373
+ its HTTPS is tried every 10 s until the code is in, and no WebSocket is opened there
374
+ meanwhile ([Inside on one address family](access.md#inside-on-one-address-family)).
368
375
  The clock offset is the server's time minus the browser's at the middle of the round
369
376
  trip, ± half the round trip.
370
377
 
@@ -116,6 +116,19 @@ links and footer credit. Three places still say texas2step:
116
116
 
117
117
  Newest first. Each release's notes have the details and its upgrade steps.
118
118
 
119
+ ### [2.3.0](https://github.com/robert-patrick-texas/texas2step/releases/tag/v2.3.0) (2026-10-25)
120
+
121
+ - [x] Passes carry when they were issued: `access_pass_hours` applies to passes already
122
+ given, and `texas2step access passes --end` ends every pass, on every server with
123
+ lockouts shared.
124
+ - [x] `texas2step access shuffle` changes the codes at once, without a new key.
125
+ - [x] A visitor inside on one address family and outside on the other is asked for the
126
+ code in a box over the page; Diag says which address is restricted.
127
+ - [x] The code screen's words are the organization's, with `access_note` under the box.
128
+ - [x] `texas2step-setup --port` for servers where another program needs 8080.
129
+ - [x] `monitor_voice` withholds Monitor's Voice call profile, as `monitor_video` does
130
+ the Video call one.
131
+
119
132
  ### [2.2.4](https://github.com/robert-patrick-texas/texas2step/releases/tag/v2.2.4) (2026-10-25)
120
133
 
121
134
  - [x] The desk's functions refuse requests made for pages on other sites; with
@@ -37,14 +37,20 @@ pass.
37
37
  the service (`0640 root:texas2step`). Anyone holding it can work out every code, so it
38
38
  is kept out of the world-readable `config.toml`. `texas2step access show` prints a
39
39
  fingerprint to compare servers by, which reveals nothing about the key.
40
- * **Passes.** A right code buys a pass for `access_pass_hours` (12 by default): an expiry
41
- time signed with the key, checked by every server that holds it. The page keeps it in
40
+ * **Passes.** A right code buys a pass for `access_pass_hours` (12 by default): the time
41
+ it was issued, signed with the key, checked by every server that holds it against its
42
+ `access_pass_hours` as it is then. The page keeps it in
42
43
  local storage, with no cookie, and sends it to its own server in an `Authorization`
43
44
  header (a WebSocket's in its subprotocol), so it stays out of URLs that proxies log;
44
45
  to the other servers, as `?pass=`. A pass is a bearer token: it isn't tied to
45
- an address or a browser, and works until it expires. A
46
+ an address or a browser, and works until it runs out. A
46
47
  [block set by hand](access.md#lockouts-and-blocks) ends it for the network blocked,
47
- WebSockets and UDP sessions already open included. A new access key ends every pass.
48
+ WebSockets and UDP sessions already open included.
49
+ [`texas2step access passes --end`](access.md#ending-every-pass) ends every pass issued
50
+ so far, on every server with lockouts shared, while the codes keep working.
51
+ [`texas2step access shuffle`](access.md#shuffling-the-codes) replaces the current and
52
+ previous codes with a new one, and with `--end-passes` ends the passes too. A new access
53
+ key ends every pass and code.
48
54
  * **Standing phrases.** For the desk away from a PC, in their own `0640` file
49
55
  (`access-phrases.toml`), with an optional last day. Each use is a journal warning with
50
56
  the phrase's name, never the phrase. The examples published in the docs are refused.
@@ -142,7 +142,9 @@ acknowledgements the server sends back, as measured over IPv6. A tab left runnin
142
142
  When the limit is reached, the server holds those sessions to the small probes of the
143
143
  Path profile, which aren't counted, and Monitor says so with the site's message and link.
144
144
  Until the limit opens again, a call profile is refused at its start.
145
- `monitor_video = false` in the settings withholds the Video call profile altogether.
145
+ `monitor_video = false` in the settings withholds the Video call profile altogether, and
146
+ `monitor_voice = false` the Voice call one: the page doesn't offer them, and the server
147
+ refuses them. With both, Monitor runs only the uncounted Path profile.
146
148
 
147
149
  ## What isn't counted
148
150
 
@@ -1,3 +1,3 @@
1
1
  """texas2step: web-centric throughput, latency, jitter and packet-loss measurement."""
2
2
 
3
- __version__ = "2.2.4"
3
+ __version__ = "2.3.0"
@@ -51,6 +51,12 @@ WORDS = Path(__file__).with_name("words.txt")
51
51
  KEY_BYTES = 32
52
52
  DRAW = 512 # built-in words each organization draws by its key
53
53
  GRACE = 24 * 3600 # the previous code still works this long after a change
54
+ # How far ahead of this server's clock a pass may be stamped: another server's clock, or
55
+ # one that issued it just after passes were ended there (Codes.issue).
56
+ PASS_AHEAD = 300
57
+ # The codes' shuffle (texas2step access shuffle): a random value, the same on every
58
+ # server, mixed into each code. Not secret: the codes still need the key.
59
+ SHUFFLE = re.compile(r"[0-9a-f]{16}")
54
60
  MIN_SITE_WORDS = 100
55
61
  MAX_ENTRY = 40
56
62
  GROUP = "texas2step" # the service's group, which must read the key
@@ -376,8 +382,9 @@ class Codes:
376
382
  return Period(f"{first:%Y-%m}", self._midnight(first), self._midnight(following))
377
383
  return Period("fixed", None, None)
378
384
 
379
- def code_for(self, period: Period) -> str:
380
- mac = self._mac("code", self.interval, period.id)
385
+ def code_for(self, period: Period, shuffle: str = "") -> str:
386
+ """The period's code; with ``shuffle``, a different one, nobody's before it."""
387
+ mac = self._mac("code", self.interval, period.id, *([shuffle] if shuffle else []))
381
388
  n = len(self.words)
382
389
  first = int.from_bytes(mac[0:8]) % n
383
390
  parts = [self.words[first]]
@@ -387,21 +394,24 @@ class Codes:
387
394
  number = int.from_bytes(mac[16:24]) % 10**self.digits
388
395
  return ".".join([*parts, f"{number:0{self.digits}d}"])
389
396
 
390
- def current(self, now: datetime) -> Current:
397
+ def current(self, now: datetime, shuffle: str = "", shuffled_at: float = 0) -> Current:
398
+ """The period's code, and the previous period's in its grace day. Shuffled in this
399
+ period, there is no previous one: the codes given before the shuffle end with it."""
391
400
  period = self.period(now)
392
401
  previous = until = None
393
- if period.start is not None and now < period.start + timedelta(seconds=GRACE):
394
- previous = self.code_for(self.period(period.start - timedelta(seconds=1)))
402
+ grace = period.start is not None and now < period.start + timedelta(seconds=GRACE)
403
+ if grace and period.start is not None and shuffled_at < period.start.timestamp():
404
+ previous = self.code_for(self.period(period.start - timedelta(seconds=1)), shuffle)
395
405
  until = period.start + timedelta(seconds=GRACE)
396
- return Current(self.code_for(period), period, previous, until)
406
+ return Current(self.code_for(period, shuffle), period, previous, until)
397
407
 
398
- def check(self, text: str, now: datetime) -> bool:
408
+ def check(self, text: str, now: datetime, shuffle: str = "", shuffled_at: float = 0) -> bool:
399
409
  """Whether ``text`` is the current code, or the previous one in its grace day."""
400
410
  given = normalize(text)
401
411
  head, _, tail = given.rpartition(".")
402
412
  if tail.isdigit() and len(tail) <= self.digits: # "7" for "07"
403
413
  given = f"{head}.{int(tail):0{self.digits}d}"
404
- current = self.current(now)
414
+ current = self.current(now, shuffle, shuffled_at)
405
415
  return any(
406
416
  hmac.compare_digest(given.encode(), code.encode())
407
417
  for code in (current.code, current.previous)
@@ -420,7 +430,7 @@ class Codes:
420
430
  def expired(self, phrase: Phrase, now: datetime) -> bool:
421
431
  return phrase.until is not None and now.astimezone(self.zone).date() > phrase.until
422
432
 
423
- def fingerprint(self) -> str:
433
+ def fingerprint(self, shuffle: str = "") -> str:
424
434
  """The same on every server that would give the same codes; reveals nothing."""
425
435
  words = hashlib.sha256("\n".join(self.words).encode()).hexdigest()
426
436
  what = (
@@ -432,27 +442,44 @@ class Codes:
432
442
  if self.phrases: # without any, as before there were phrases
433
443
  standing = sorted(f"{p.code}|{p.until or ''}" for p in self.phrases)
434
444
  what += (hashlib.sha256("\n".join(standing).encode()).hexdigest(),)
445
+ if shuffle: # without one, as before codes could be shuffled
446
+ what += (shuffle,)
435
447
  mac = self._mac("fingerprint", *what, words).hex()
436
448
  return f"{mac[0:4]}-{mac[4:8]}-{mac[8:12]}"
437
449
 
438
- def issue(self, now: float | None = None) -> tuple[str, int]:
439
- expires = int(now if now is not None else time.time()) + self.pass_seconds
440
- return f"{expires}.{self._sign(expires)}", expires
441
-
442
- def _sign(self, expires: int) -> str:
443
- mac = self._mac("pass", str(expires))[:18]
450
+ # A pass is "<issued>.<signature>": when it was issued, in Unix seconds, signed with
451
+ # the key. How long it lasts is decided when it is checked, by access_pass_hours as
452
+ # it is then, so a shorter setting shortens passes already given and a longer one
453
+ # lengthens them. Passes issued before passes were last ended are refused.
454
+
455
+ def issue(self, now: float | None = None, ended: float = 0) -> tuple[str, int]:
456
+ """A new pass, and when it runs out here. Stamped after ``ended``, when passes
457
+ were last ended, even by a clock a little behind the server that ended them."""
458
+ issued = max(int(now if now is not None else time.time()), int(ended) + 1)
459
+ return f"{issued}.{self._sign(issued)}", issued + self.pass_seconds
460
+
461
+ def _sign(self, issued: int) -> str:
462
+ # Not the "pass" of earlier versions, which signed the expiry: such a pass must
463
+ # not be read as one issued at that time.
464
+ mac = self._mac("pass-issued", str(issued))[:18]
444
465
  return base64.urlsafe_b64encode(mac).decode()
445
466
 
446
- def pass_expires(self, token: str, now: float | None = None) -> int | None:
447
- """The pass's expiry, if it is ours, current, and no longer than passes are."""
467
+ def pass_issued(self, token: str, now: float | None = None, ended: float = 0) -> int | None:
468
+ """When the pass was issued, if it is ours and still good: issued after ``ended``,
469
+ within access_pass_hours, and not more than PASS_AHEAD seconds ahead of now."""
448
470
  stamp, _, sig = (token or "").partition(".")
449
471
  if not stamp.isdigit() or len(stamp) > 12:
450
472
  return None
451
- expires = int(stamp)
473
+ issued = int(stamp)
452
474
  now = now if now is not None else time.time()
453
- if not now < expires <= now + self.pass_seconds + 300:
475
+ if issued <= ended or issued > now + PASS_AHEAD or now >= issued + self.pass_seconds:
454
476
  return None
455
- return expires if hmac.compare_digest(sig.encode(), self._sign(expires).encode()) else None
477
+ return issued if hmac.compare_digest(sig.encode(), self._sign(issued).encode()) else None
478
+
479
+ def pass_expires(self, token: str, now: float | None = None, ended: float = 0) -> int | None:
480
+ """When the pass runs out here, if it is still good (pass_issued)."""
481
+ issued = self.pass_issued(token, now, ended)
482
+ return None if issued is None else issued + self.pass_seconds
456
483
 
457
484
 
458
485
  # --- Who is inside -----------------------------------------------------------------