windscout 0.6.1 → 0.6.4
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.
- package/README.md +39 -20
- package/dist/cli.js +21 -10
- package/dist/config.js +4 -1
- package/dist/docker.js +10 -4
- package/dist/probe.js +7 -0
- package/dist/tui/picker.js +33 -13
- package/dist/tui/progress.js +3 -1
- package/dist/tui/report.js +10 -1
- package/dist/tui/tables.js +19 -9
- package/dist/windscribe.js +13 -0
- package/dist/worker.js +3 -2
- package/package.json +2 -4
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# WindScout
|
|
2
2
|
|
|
3
|
-
Find which Windscribe locations and protocols actually work from your network, ranked by
|
|
3
|
+
Find which Windscribe locations and protocols actually work from your network, ranked by latency. Runs on Linux, macOS and Windows.
|
|
4
4
|
|
|
5
|
-
WindScout drives the official `windscribe-cli`: it connects to each country × protocol pair, measures latency
|
|
5
|
+
WindScout drives the official `windscribe-cli`: it connects to each country × protocol pair, measures latency through the tunnel, disconnects, and prints a ranked table.
|
|
6
6
|
|
|
7
7
|

|
|
8
8
|
|
|
@@ -30,7 +30,7 @@ Run `windscout` with no arguments to open the interactive picker:
|
|
|
30
30
|
```
|
|
31
31
|
╭─ WindScout ──────────────────────────────────── 2/68 selected ─╮
|
|
32
32
|
│ 1 ● wireguard 2 ● stealth 3 ○ wstunnel 4 ● udp 5 ● tcp │
|
|
33
|
-
│
|
|
33
|
+
│ p ○ all ports off: each protocol on its default port │
|
|
34
34
|
╰────────────────────────────────────────────────────────────────╯
|
|
35
35
|
╭────────────────────────────────────────────────────────────────╮
|
|
36
36
|
│ 🔍 an▏ 1–16 of 18 │
|
|
@@ -51,7 +51,7 @@ Run `windscout` with no arguments to open the interactive picker:
|
|
|
51
51
|
| `/` | search by country name or code (enter/esc to finish, esc again to clear) |
|
|
52
52
|
| `a` / `n` | select all / unselect all (only the matching countries while a search is active) |
|
|
53
53
|
| `1`–`6` | toggle a protocol (`1`–`5` on Linux, which has no IKEv2) |
|
|
54
|
-
| `
|
|
54
|
+
| `p` | toggle [all ports](#all-ports): each protocol on every port it has, not just its default |
|
|
55
55
|
| `f` | toggle "block internet while scanning" (firewall) |
|
|
56
56
|
| `r` | toggle random order: scan the selected countries shuffled, a new order each run |
|
|
57
57
|
| `d` | toggle the [parallel scan in docker containers](#parallel-scan-with-docker) (installs or starts docker first, after asking) |
|
|
@@ -59,11 +59,11 @@ Run `windscout` with no arguments to open the interactive picker:
|
|
|
59
59
|
| `enter` | start scanning |
|
|
60
60
|
| `q` | quit (during a scan: stop and show partial results; press again to force quit) |
|
|
61
61
|
|
|
62
|
-
**Mouse:** click a country to select or unselect it, click a protocol or
|
|
62
|
+
**Mouse:** click a country to select or unselect it, click a protocol or a setting's line to toggle it, click the search box to start typing (click anywhere else to leave it), and scroll the list with the wheel. The `[ ▶ Start ] [ All ] [ None ] [ Quit ]` buttons under the table do the same as their keys; the shortcut key is underlined on screen (on the buttons, the protocol numbers and the settings' letters). This needs a terminal with mouse support, which most have. On Windows, use Windows Terminal; if clicks do nothing there, the keyboard does everything.
|
|
63
63
|
|
|
64
64
|
Your selection is saved to `~/.config/windscout/config.json` (`%USERPROFILE%\.config\windscout\config.json` on Windows) and preselected next time.
|
|
65
65
|
|
|
66
|
-
While scanning, WindScout shows a progress bar, elapsed time and remaining time. It also shows what it's doing right now (connecting
|
|
66
|
+
While scanning, WindScout shows a progress bar, elapsed time and remaining time. It also shows what it's doing right now (connecting or measuring latency). The table of working locations refreshes after each country. When the scan finishes, the final report is printed to your normal terminal. It ends with the best connection and the other working ones, each with its `windscribe-cli connect` command. At the prompt, press enter to connect to the best one, type another one's number to connect to that, or `q` to quit.
|
|
67
67
|
|
|
68
68
|
### Non-interactive
|
|
69
69
|
|
|
@@ -72,6 +72,7 @@ Pass `-c` to skip the picker, for scripts or when there is no terminal:
|
|
|
72
72
|
```sh
|
|
73
73
|
windscout -c DE,NL,TR -p stealth,tcp:443 # chosen countries and protocols (port optional)
|
|
74
74
|
windscout -c Frankfurt,Wurstchen # cities or datacenter nicknames work too
|
|
75
|
+
windscout -c DE -p stealth,tcp -P # every port of stealth and tcp
|
|
75
76
|
windscout -t 40 -a # longer connect timeout, also list failed attempts
|
|
76
77
|
```
|
|
77
78
|
|
|
@@ -79,19 +80,17 @@ windscout -t 40 -a # longer connect timeout, also list
|
|
|
79
80
|
|---|---|---|
|
|
80
81
|
| `-c, --countries` | comma-separated ISO country codes, city names or nicknames | open the picker |
|
|
81
82
|
| `-p, --protocols` | comma-separated: `wireguard`, `ikev2` (macOS and Windows), `stealth`, `wstunnel`, `udp`, `tcp`, optionally `:port` | saved, or all |
|
|
83
|
+
| `-P, --all-ports` | try each protocol on [every port](#all-ports) Windscribe offers for it, not just its default | picker: saved, or off; with `-c`: off |
|
|
82
84
|
| `-t, --timeout` | seconds to wait for each connection, at least 10 | picker: saved, or 15; with `-c`: 15 |
|
|
83
85
|
| `-a, --all` | include failed attempts in the final table | off |
|
|
84
|
-
| `-n, --no-speed` | connect-only mode: skip the download test, rank by latency | picker: saved, or connect-only on first run; with `-c`: speed test on |
|
|
85
86
|
| `-f, --firewall` | block internet while scanning, except through the VPN | picker: saved, or off; with `-c`: off |
|
|
86
87
|
| `-r, --random` | scan the countries in a random order | picker: saved, or off; with `-c`: off |
|
|
87
88
|
| `-d, --docker` | [scan in parallel, in docker containers](#parallel-scan-with-docker) | picker: saved, or off; with `-c`: off |
|
|
88
89
|
| `-v, --version` | print the installed version; if a newer one is on npm, show the update command (with `sudo` when needed) and ask (y/n) to run it | |
|
|
89
90
|
|
|
90
|
-
##
|
|
91
|
+
## Availability grid
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
Both modes show an **availability grid**: one row per country, and ✓ / ✗ for each protocol (`·` = not tried yet). Location(s) lists each datacenter that connected, one per line, since protocols can land on different ones:
|
|
93
|
+
Each working connection gets a quick latency test, and the working ones are ranked by latency. The scan screen and the final report show an **availability grid**: one row per country, and ✓ / ✗ for each protocol (`·` = not tried yet; `key limit`, in a [parallel scan](#parallel-scan-with-docker): WireGuard couldn't be tried). With [all ports](#all-ports), it counts how many of a protocol's ports worked: `✓ 3/11`. Location(s) lists each datacenter that connected, one per line, since protocols can land on different ones:
|
|
95
94
|
|
|
96
95
|
```
|
|
97
96
|
╭──────────────────────────────────────────────────────────────────────────────╮
|
|
@@ -106,7 +105,25 @@ Both modes show an **availability grid**: one row per country, and ✓ / ✗ for
|
|
|
106
105
|
╰─────────────────────────┴────────────────────────┴─────────┴───────────┴─────╯
|
|
107
106
|
```
|
|
108
107
|
|
|
109
|
-
While scanning, press `tab` to switch between the availability grid and the
|
|
108
|
+
While scanning, press `tab` to switch between the availability grid and the latency ranking. Press `p` (or `space`) to pause: the current attempt finishes, the VPN disconnects, and the scan waits until you press `p` again. Paused time doesn't count toward elapsed or remaining. The scan starts on the availability grid.
|
|
109
|
+
|
|
110
|
+
## All ports
|
|
111
|
+
|
|
112
|
+
Each protocol is normally tried on its default port, the first one Windscribe lists for it. A network can block a protocol on one port and let it through on another, so turn on **all ports** with `p` in the picker or `-P` (`--all-ports`): each selected protocol is then tried on every port Windscribe offers for it. WindScout asks your Windscribe app for the ports (`windscribe-cli ports tcp`), so the list follows Windscribe's. In October 2026 it was:
|
|
113
|
+
|
|
114
|
+
| Protocol | Ports | Attempts per country |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| wireguard | 443, 80, 53, 123, 1194, 65142 | 6 |
|
|
117
|
+
| stealth | 443, 587, 21, 22, 80, 123, 143, 3306, 8080, 54783, 8443 | 11 |
|
|
118
|
+
| wstunnel | 443 | 1 |
|
|
119
|
+
| udp | 443, 80, 53, 123, 1194, 54783 | 6 |
|
|
120
|
+
| tcp | 443, 587, 21, 22, 80, 123, 143, 3306, 8080, 54783, 1194 | 11 |
|
|
121
|
+
|
|
122
|
+
With every protocol selected, that's 35 attempts per country instead of 5, 7 times as many (on macOS and Windows, IKEv2 adds its own ports). The picker shows the count (`35 attempts per country, not 5`) and the estimate: one country takes about 11m 40s instead of 1m 40s in a normal scan, and about 2 minutes in a [parallel scan](#parallel-scan-with-docker), which helps most here. To try fewer, select only the protocols you need, or give a port yourself: a protocol with a port (`-p tcp:443`) stays on that one.
|
|
123
|
+
|
|
124
|
+
The availability grid keeps one column per protocol and counts how many of its ports worked (`✓ 3/11`, or `✗ 0/11` when none did). The results list each working protocol with its port, and the per-protocol table has a line per port.
|
|
125
|
+
|
|
126
|
+
When the app making an attempt doesn't offer a port, Windscribe quietly connects on the protocol's default port instead. That can happen in a parallel scan, where each container runs its own copy of the app, which can be older or newer than yours. WindScout checks the port it actually connected on and shows those attempts as `no port` (in yellow), rather than as results for a port that wasn't tried. Listing ports needs Windscribe 2.22 or newer. With an older app, all ports changes nothing, and the picker says so.
|
|
110
127
|
|
|
111
128
|
## Block internet while scanning (firewall)
|
|
112
129
|
|
|
@@ -116,7 +133,7 @@ Between attempts the VPN is disconnected for a moment, and apps on your computer
|
|
|
116
133
|
|
|
117
134
|
A normal scan tries one location at a time, because your computer has one VPN connection. Turn on **parallel scan** with `d` in the picker or `--docker`, and each attempt runs in its own docker container instead. Each container has its own Windscribe app (Windscribe's Linux CLI app) and so its own VPN connection, and several locations are tried at once. The results come back to the scan screen, which shows a line per container.
|
|
118
135
|
|
|
119
|
-
**How many containers:** one per attempt,
|
|
136
|
+
**How many containers:** one per attempt, so 1 country with 5 protocols starts 5. Docker's CPUs, three quarters of its memory (at 250 MB each) and a maximum of 16 limit that. With more attempts than that, WindScout starts the fewest containers that need no more rounds: 17 attempts get 9 containers with 2 attempts each, the same 2 rounds that 16 containers would take. Fewer containers log in sooner (in a test, 1 took 37 s, and 16 together up to 75 s) and register fewer WireGuard keys. The picker shows the number (`9 at a time`) and the estimate that goes with it, which includes the time the containers take to log in before the scan starts (`+ ~58s login`: about 35 s, plus 2.5 s per container).
|
|
120
137
|
|
|
121
138
|
**First time:** if docker isn't installed or isn't running, `d` (or a scan with `--docker`) opens a dialog that says what WindScout will do, and does it once you agree:
|
|
122
139
|
|
|
@@ -142,9 +159,11 @@ The mirrors come from [Mirava](https://github.com/MiravaOrg/Mirava)'s list of mi
|
|
|
142
159
|
|
|
143
160
|
**Login:** the containers' Windscribe apps log in with the session that the Windscribe app on this computer is logged in with, so they never log in with your password. Windscribe locks an account that logs in many times at once, and a login per container would do that. WindScout reads the session from the app's settings (`~/.config/Windscribe/Windscribe2.conf` on Linux, the `com.windscribe.Windscribe2` defaults on macOS, `HKEY_CURRENT_USER\Software\Windscribe\Windscribe2` in the registry on Windows) and hands it to each container on its standard input, not on a command line, where other programs could see it. Each container's docker volume (`windscout-1`, `windscout-2`, …) keeps its app's settings for the next scans, with the WireGuard keys it registered with your account. A volume that another account used starts over. If you log out of the Windscribe app, the containers' session ends too, and the next scan gives them the new one.
|
|
144
161
|
|
|
162
|
+
**WireGuard keys:** each container's app registers a WireGuard key of its own with your account the first time it uses WireGuard, and keeps it in its volume for later scans. Windscribe allows each account only so many keys. A container past that limit can't try WireGuard, which says nothing about your network, so the grid shows `key limit` (in yellow) instead of ✗, and those attempts don't count in the report's per-protocol numbers. Under the grid, the report says how many there were and gives the command that tries them on this computer, which has a key of its own: `windscout -c "DE,NL" -p wireguard`.
|
|
163
|
+
|
|
145
164
|
WindScout 0.6.0 asked for your username and password instead, and could save them in `~/.config/windscout/login.json`. WindScout now deletes that file.
|
|
146
165
|
|
|
147
|
-
**Measuring:** the containers share your internet connection, so they take turns measuring latency
|
|
166
|
+
**Measuring:** the containers share your internet connection, so they take turns measuring latency while the connecting happens in parallel. Numbers stay comparable with a normal scan. Most of the time saved comes from attempts that hang until the timeout, which overlap now.
|
|
148
167
|
|
|
149
168
|
**Differences from a normal scan:**
|
|
150
169
|
|
|
@@ -164,23 +183,23 @@ While scanning:
|
|
|
164
183
|
│ ██████████████████████████████████████████▌░░░░░░░░░░░░░░░░ 72% │
|
|
165
184
|
│ │
|
|
166
185
|
│ ⠏ 🇳🇱 Netherlands · stealth │
|
|
167
|
-
│ measuring
|
|
186
|
+
│ measuring latency · Amsterdam - Tulip · 1s │
|
|
168
187
|
├──────────────────────────────────────────────────────────────────────┤
|
|
169
188
|
│ elapsed 25s │ remaining ≈ 13s │ ✓ 2 working │ ✗ 1 blocked │
|
|
170
189
|
╰──────────────────────────────────────────────────────────────────────╯
|
|
171
190
|
╭──────────────────────────────────────────────────────────────────────╮
|
|
172
191
|
│ Working so far 2 found │
|
|
173
192
|
├───┬────────────┬───────────────────────┬───────────┬───────────┬─────┤
|
|
174
|
-
│ # │ Country │ Location │ Protocol │
|
|
193
|
+
│ # │ Country │ Location │ Protocol │ Latency │ ... │
|
|
175
194
|
```
|
|
176
195
|
|
|
177
196
|
At the end, the results, a per-protocol summary and the best pick are printed to your normal terminal:
|
|
178
197
|
|
|
179
198
|
```
|
|
180
|
-
╭─ ★ Best
|
|
181
|
-
│ 🇳🇱 Amsterdam - Tulip over wireguard ·
|
|
182
|
-
│ windscribe-cli connect "Tulip" wireguard
|
|
183
|
-
|
|
199
|
+
╭─ ★ Best (lowest latency) ────────────────────╮
|
|
200
|
+
│ 🇳🇱 Amsterdam - Tulip over wireguard · 109 ms │
|
|
201
|
+
│ windscribe-cli connect "Tulip" wireguard │
|
|
202
|
+
╰──────────────────────────────────────────────╯
|
|
184
203
|
```
|
|
185
204
|
|
|
186
205
|
## Notes
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// WindScout: find which Windscribe locations and protocols actually work from here.
|
|
3
3
|
// Drives the official windscribe-cli: connects to each country/protocol pair,
|
|
4
|
-
// measures latency
|
|
4
|
+
// measures latency through the tunnel, prints a ranked table.
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
6
6
|
import { spawnSync } from "node:child_process";
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
|
-
import { CONFIG, DEFAULT_TIMEOUT, forgetLogin, loadConfig, loadDockerWireGuard, MIN_TIMEOUT, saveDockerWireGuard } from "./config.js";
|
|
8
|
+
import { CONFIG, DEFAULT_TIMEOUT, forgetLogin, loadConfig, loadDockerWireGuard, MIN_TIMEOUT, saveDockerWireGuard, SPEED_TEST } from "./config.js";
|
|
9
9
|
import { notInContainers, stopWorkers } from "./docker.js";
|
|
10
10
|
import { probe } from "./probe.js";
|
|
11
11
|
import { Scan } from "./scan.js";
|
|
@@ -17,27 +17,28 @@ import { leaveScreen, out } from "./tui/terminal.js";
|
|
|
17
17
|
import { cyan, green, yellow } from "./tui/text.js";
|
|
18
18
|
import { now, sleep } from "./time.js";
|
|
19
19
|
import { canUpdate, checkUpdate, runUpdate, updateNotice, versionLines } from "./update.js";
|
|
20
|
-
import { blockInternet, CLI, connectState, isFirewallOn, PROTOCOLS, restoreFirewall, waitForLogin, windscribe } from "./windscribe.js";
|
|
20
|
+
import { blockInternet, CLI, connectState, isFirewallOn, protocolPorts, PROTOCOLS, restoreFirewall, waitForLogin, windscribe, withPorts } from "./windscribe.js";
|
|
21
21
|
import { worker } from "./worker.js";
|
|
22
22
|
const HELP = `Usage: windscout [options]
|
|
23
23
|
|
|
24
|
-
Find which Windscribe locations and protocols work from your network, ranked by speed.
|
|
24
|
+
Find which Windscribe locations and protocols work from your network, ranked by ${SPEED_TEST ? "speed" : "latency"}.
|
|
25
25
|
Without -c it opens an interactive country picker (your selection is remembered).
|
|
26
26
|
|
|
27
27
|
Options:
|
|
28
28
|
-c, --countries LIST comma-separated ISO codes, city names or nicknames (skips the picker)
|
|
29
29
|
-p, --protocols LIST comma-separated protocols, optional :port (default: ${PROTOCOLS.join(",")})
|
|
30
|
+
-P, --all-ports try each protocol on every port Windscribe offers for it, not just its default
|
|
30
31
|
-t, --timeout SEC seconds to wait for each connection, at least ${MIN_TIMEOUT} (default: saved, or ${DEFAULT_TIMEOUT})
|
|
31
32
|
-a, --all also show failed attempts in the final table
|
|
32
|
-
-n, --no-speed connect-only: skip the download test, rank by latency (faster scan)
|
|
33
|
-
-f, --firewall block internet while scanning, except through the VPN (turned back off afterwards)
|
|
33
|
+
${SPEED_TEST ? " -n, --no-speed connect-only: skip the download test, rank by latency (faster scan)\n" : ""} -f, --firewall block internet while scanning, except through the VPN (turned back off afterwards)
|
|
34
34
|
-r, --random scan the countries in a random order
|
|
35
|
-
-d, --docker scan in parallel, in docker containers (
|
|
35
|
+
-d, --docker scan in parallel, in docker containers (one per attempt, up to 16, as docker's CPUs and memory allow)
|
|
36
36
|
-v, --version show the installed version (and offer to update if a newer one is out)
|
|
37
37
|
-h, --help show this help
|
|
38
38
|
|
|
39
39
|
Selection is stored in ${CONFIG}
|
|
40
|
-
|
|
40
|
+
Examples: windscout -c DE,NL,TR -p stealth,tcp:443
|
|
41
|
+
windscout -c DE -p stealth,tcp -P`;
|
|
41
42
|
// "DE, NL,,TR" → ["DE", "NL", "TR"]
|
|
42
43
|
const list = (s) => s.split(",").map((x) => x.trim()).filter(Boolean);
|
|
43
44
|
const ask = (question) => new Promise((resolve) => {
|
|
@@ -83,11 +84,12 @@ async function main() {
|
|
|
83
84
|
options: {
|
|
84
85
|
countries: { type: "string", short: "c" },
|
|
85
86
|
protocols: { type: "string", short: "p" },
|
|
87
|
+
"all-ports": { type: "boolean", short: "P", default: false },
|
|
86
88
|
timeout: { type: "string", short: "t" },
|
|
87
89
|
all: { type: "boolean", short: "a", default: false },
|
|
88
90
|
help: { type: "boolean", short: "h", default: false },
|
|
89
91
|
version: { type: "boolean", short: "v", default: false },
|
|
90
|
-
"no-speed": { type: "boolean", short: "n", default: false },
|
|
92
|
+
"no-speed": { type: "boolean", short: "n", default: false }, // still accepted while the speed test is hidden
|
|
91
93
|
firewall: { type: "boolean", short: "f", default: false },
|
|
92
94
|
random: { type: "boolean", short: "r", default: false },
|
|
93
95
|
docker: { type: "boolean", short: "d", default: false },
|
|
@@ -116,9 +118,10 @@ async function main() {
|
|
|
116
118
|
const tty = Boolean(process.stdin.isTTY && out.isTTY);
|
|
117
119
|
let countries = a.countries ? list(a.countries) : undefined;
|
|
118
120
|
let protocols = a.protocols ? list(a.protocols) : undefined;
|
|
119
|
-
let speedTest = !a["no-speed"];
|
|
121
|
+
let speedTest = SPEED_TEST && !a["no-speed"];
|
|
120
122
|
let firewall = a.firewall;
|
|
121
123
|
let random = a.random;
|
|
124
|
+
let allPorts = a["all-ports"];
|
|
122
125
|
let docker = a.docker;
|
|
123
126
|
if (!countries) {
|
|
124
127
|
if (!tty)
|
|
@@ -132,6 +135,7 @@ async function main() {
|
|
|
132
135
|
// already on in the app: show it on, since the scan won't turn it off
|
|
133
136
|
firewall: firewall || isFirewallOn(status) || (saved?.firewall ?? false),
|
|
134
137
|
random: random || (saved?.random ?? false),
|
|
138
|
+
allPorts: allPorts || (saved?.allPorts ?? false),
|
|
135
139
|
docker: docker || (saved?.docker ?? false),
|
|
136
140
|
});
|
|
137
141
|
if (!choice)
|
|
@@ -143,11 +147,18 @@ async function main() {
|
|
|
143
147
|
speedTest = choice.speed;
|
|
144
148
|
firewall = choice.firewall;
|
|
145
149
|
random = choice.random;
|
|
150
|
+
allPorts = choice.allPorts;
|
|
146
151
|
timeout = choice.timeout;
|
|
147
152
|
docker = choice.docker;
|
|
148
153
|
}
|
|
149
154
|
protocols ??= PROTOCOLS;
|
|
150
155
|
timeout ??= DEFAULT_TIMEOUT;
|
|
156
|
+
// the picker asked for the ports already: waiting for that keeps windscribe-cli free for the scan
|
|
157
|
+
if (allPorts || !a.countries) {
|
|
158
|
+
const lists = await protocolPorts();
|
|
159
|
+
if (allPorts)
|
|
160
|
+
protocols = withPorts(protocols, lists);
|
|
161
|
+
}
|
|
151
162
|
if (random)
|
|
152
163
|
countries = countries.map((c) => [Math.random(), c]).sort(([x], [y]) => x - y).map(([, c]) => c);
|
|
153
164
|
let probers = [probe], after; // after: what runs here once the containers are done
|
package/dist/config.js
CHANGED
|
@@ -3,6 +3,8 @@ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { COUNTRIES, PROTOCOLS } from "./windscribe.js";
|
|
6
|
+
// The speed test is hidden: every scan is connect-only, and a saved "on" reads as off. Its code stays; true brings it back
|
|
7
|
+
export const SPEED_TEST = false;
|
|
6
8
|
export const DEFAULT_TIMEOUT = 15;
|
|
7
9
|
export const MIN_TIMEOUT = 10; // seconds; shorter cuts off connections that would have worked
|
|
8
10
|
export const CONFIG = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "windscout", "config.json");
|
|
@@ -14,9 +16,10 @@ export function loadConfig() {
|
|
|
14
16
|
return {
|
|
15
17
|
countries: (c.countries ?? []).filter((x) => COUNTRIES.includes(x)),
|
|
16
18
|
protocols: (c.protocols ?? []).filter((x) => PROTOCOLS.includes(x)),
|
|
17
|
-
speed: c.speed !== false,
|
|
19
|
+
speed: SPEED_TEST && c.speed !== false,
|
|
18
20
|
firewall: c.firewall === true,
|
|
19
21
|
random: c.random === true,
|
|
22
|
+
allPorts: c.allPorts === true,
|
|
20
23
|
timeout: c.timeout >= MIN_TIMEOUT ? c.timeout : DEFAULT_TIMEOUT,
|
|
21
24
|
docker: c.docker === true,
|
|
22
25
|
};
|
package/dist/docker.js
CHANGED
|
@@ -105,12 +105,18 @@ export async function dockerState() {
|
|
|
105
105
|
return "ready";
|
|
106
106
|
}
|
|
107
107
|
const WORKER_MEMORY = 250e6; // bytes per container: Windscribe's app and helper, a tunnel and node take ~120 MB
|
|
108
|
-
|
|
108
|
+
// ponytail: past this, more containers mostly add load on the one uplink, and WireGuard keys on the account; raise if real scans say so
|
|
109
|
+
const MAX_WORKERS = 16;
|
|
109
110
|
/** Protocols the containers can't do, which a parallel scan runs on this computer after them: IKEv2 (Windscribe's Linux app
|
|
110
111
|
* has none), and WireGuard when their kernel can't run its routing (see DOCKERFILE) */
|
|
111
|
-
export const notInContainers = (protocols, wireguard) => new Set(protocols.filter((p) => p
|
|
112
|
-
/** Containers for
|
|
113
|
-
|
|
112
|
+
export const notInContainers = (protocols, wireguard) => new Set(protocols.filter((p) => /^ikev2(:|$)/.test(p) || (!wireguard && /^wireguard(:|$)/.test(p))));
|
|
113
|
+
/** Containers for this many attempts, when docker can run at most `most`: one per attempt; past that, the fewest that need no
|
|
114
|
+
* more rounds (17 attempts, 16 at most: 9 containers make 2 rounds, as 16 would), since fewer log in sooner and register fewer keys */
|
|
115
|
+
export const containersFor = (attempts, most) => attempts <= most ? Math.max(1, attempts) : Math.ceil(attempts / Math.ceil(attempts / most));
|
|
116
|
+
// ponytail: rough seconds until n containers are logged in: in a test, 1 took 37 s, 16 at once 38-75 s; refine from real runs
|
|
117
|
+
export const loginSeconds = (containers) => 35 + 2.5 * containers;
|
|
118
|
+
/** Containers for a scan of this many attempts, within docker's CPUs and 3/4 of its memory */
|
|
119
|
+
export const workerCount = (attempts) => containersFor(attempts, Math.max(1, Math.min(MAX_WORKERS, info.cpus, Math.floor((info.memory * 0.75) / WORKER_MEMORY))));
|
|
114
120
|
const DMG = `https://desktop.docker.com/mac/main/${process.arch === "arm64" ? "arm64" : "amd64"}/Docker.dmg`;
|
|
115
121
|
const START_DESKTOP = process.platform === "win32"
|
|
116
122
|
? { label: "start Docker Desktop", cmd: () => `start "" "${DESKTOP}"` }
|
package/dist/probe.js
CHANGED
|
@@ -27,11 +27,18 @@ export async function probe(query, protocol, { timeout, speedTest, stopped = ()
|
|
|
27
27
|
}
|
|
28
28
|
else if (s.includes("does not exist"))
|
|
29
29
|
return end("no such location");
|
|
30
|
+
// Windscribe refused this app another WireGuard key for the account: says nothing about this network
|
|
31
|
+
else if (s.includes("WireGuard key limit") && now() - t > 3)
|
|
32
|
+
return end("key limit");
|
|
30
33
|
else if (s === "Disconnected" && now() - t > 3)
|
|
31
34
|
return end("failed");
|
|
32
35
|
onStep(`connecting ${r.location}`.trim());
|
|
33
36
|
await sleep(1000);
|
|
34
37
|
}
|
|
38
|
+
// a port the app doesn't offer for the protocol: it connects on the protocol's default instead, so this port wasn't tried
|
|
39
|
+
const port = protocol.split(":")[1], shown = (await windscribe("status")).match(/^Protocol: .*:(\d+)\s*$/m)?.[1];
|
|
40
|
+
if (port && shown && shown !== port)
|
|
41
|
+
return end("no such port");
|
|
35
42
|
await measure(async () => {
|
|
36
43
|
onStep(`measuring latency · ${r.location}`);
|
|
37
44
|
r.latencyMs = await latencyMs();
|
package/dist/tui/picker.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { loadDockerWireGuard, MIN_TIMEOUT, saveConfig } from "../config.js";
|
|
2
|
-
import { dockerState, notInContainers, workerCount } from "../docker.js";
|
|
1
|
+
import { loadDockerWireGuard, MIN_TIMEOUT, saveConfig, SPEED_TEST } from "../config.js";
|
|
2
|
+
import { dockerState, loginSeconds, notInContainers, workerCount } from "../docker.js";
|
|
3
3
|
import { attemptSeconds, estimate } from "../scan.js";
|
|
4
4
|
import { duration } from "../time.js";
|
|
5
5
|
import { canUpdate, checkUpdate, versionLines } from "../update.js";
|
|
6
|
-
import { COUNTRIES, PROTOCOLS } from "../windscribe.js";
|
|
6
|
+
import { COUNTRIES, protocolPorts, PROTOCOLS } from "../windscribe.js";
|
|
7
7
|
import { countryName, flag } from "./country.js";
|
|
8
8
|
import { setupDocker } from "./docker.js";
|
|
9
9
|
import { draw, enterScreen, input, leaveScreen, out, repoLink } from "./terminal.js";
|
|
@@ -11,18 +11,21 @@ import { bold, cyan, dim, green, under, vw, yellow } from "./text.js";
|
|
|
11
11
|
import { box, centerIn, grid } from "./widgets.js";
|
|
12
12
|
const flip = (set, value) => (set.has(value) ? set.delete(value) : set.add(value));
|
|
13
13
|
/** Resolves to the choice (also saved for next time), null when the user quits, or "update" for u */
|
|
14
|
-
export function pick(saved, { timeout, speedTest, firewall, random, docker }) {
|
|
14
|
+
export function pick(saved, { timeout, speedTest, firewall, random, allPorts, docker }) {
|
|
15
15
|
const selected = new Set(saved?.countries ?? []);
|
|
16
16
|
const protos = new Set(saved?.protocols.length ? saved.protocols : PROTOCOLS);
|
|
17
17
|
let cursor = 0, top = 0, message = "", query = "", searching = false;
|
|
18
18
|
let seconds = String(timeout), editing = false; // the timeout, as typed
|
|
19
19
|
let dockerAt = null, inDialog = false; // docker's state once checked; a docker dialog has the screen
|
|
20
|
+
let portLists = null; // each protocol's ports, once the app said
|
|
20
21
|
let hit;
|
|
21
22
|
const visible = () => {
|
|
22
23
|
const q = query.toLowerCase();
|
|
23
24
|
return q ? COUNTRIES.filter((c) => c.toLowerCase().includes(q) || countryName(c).toLowerCase().includes(q)) : COUNTRIES;
|
|
24
25
|
};
|
|
25
|
-
const
|
|
26
|
+
const ports = (p) => (allPorts && portLists?.get(p)?.length) || 1; // attempts per country for the protocol
|
|
27
|
+
const perCountry = (ps) => [...ps].reduce((n, p) => n + ports(p), 0);
|
|
28
|
+
const attempts = () => selected.size * perCountry(protos);
|
|
26
29
|
const wireguard = loadDockerWireGuard(); // in docker containers, as the last parallel scan found
|
|
27
30
|
enterScreen();
|
|
28
31
|
return new Promise((resolve) => {
|
|
@@ -40,6 +43,12 @@ export function pick(saved, { timeout, speedTest, firewall, random, docker }) {
|
|
|
40
43
|
top = cursor - height + 1;
|
|
41
44
|
top = Math.max(0, Math.min(top, matches.length - height));
|
|
42
45
|
const protoLine = PROTOCOLS.map((p, i) => `${dim(under(String(i + 1)))} ${protos.has(p) ? green("● " + p) : dim("○ " + p)}`).join(" ");
|
|
46
|
+
const listed = portLists && [...portLists.values()].some((l) => l.length), each = perCountry(protos);
|
|
47
|
+
const portsLine = `${dim(under("p"))} ${allPorts ? green("● all ports") : dim("○ all ports")} ${dim(!allPorts
|
|
48
|
+
? "off: each protocol on its default port"
|
|
49
|
+
: !portLists ? "checking the ports…"
|
|
50
|
+
: !listed ? "this Windscribe app can't list its ports: 2.22 and newer can"
|
|
51
|
+
: `every port of each protocol: ${each} attempt${each === 1 ? "" : "s"} per country${each > protos.size ? `, not ${protos.size}` : ""}`)}`;
|
|
43
52
|
const speedLine = `${dim(under("s"))} ${speedTest ? green("● speed test") : dim("○ speed test")} ${dim(speedTest
|
|
44
53
|
? "ranks by download speed · adds ~15s per working connection"
|
|
45
54
|
: "off: connect-only, ranks by latency")}`;
|
|
@@ -55,8 +64,10 @@ export function pick(saved, { timeout, speedTest, firewall, random, docker }) {
|
|
|
55
64
|
const timeoutLine = `${dim(under("t"))} timeout ${editing ? bold(seconds) + cyan("▏") : (short ? yellow : bold)(seconds || "?")} s ${dim(`per connection · at least ${MIN_TIMEOUT}`)}`;
|
|
56
65
|
const n = attempts();
|
|
57
66
|
const here = PROTOCOLS.filter((p) => docker && protos.has(p) && notInContainers(PROTOCOLS, wireguard).has(p)); // after the containers
|
|
58
|
-
const inContainers = selected.size * (protos.
|
|
59
|
-
const
|
|
67
|
+
const inContainers = selected.size * perCountry([...protos].filter((p) => !here.includes(p)));
|
|
68
|
+
const parallel = docker && dockerAt === "ready" && inContainers > 0;
|
|
69
|
+
const slots = parallel ? workerCount(inContainers) : 1;
|
|
70
|
+
const login = parallel ? loginSeconds(slots) : 0; // the containers log in before the scan starts
|
|
60
71
|
const dockerLabel = "parallel scan in docker containers";
|
|
61
72
|
const dockerLine = `${dim(under("d"))} ${docker ? green("● " + dockerLabel) : dim("○ " + dockerLabel)} ${dim(!docker
|
|
62
73
|
? "off: one attempt at a time"
|
|
@@ -82,9 +93,10 @@ export function pick(saved, { timeout, speedTest, firewall, random, docker }) {
|
|
|
82
93
|
["quit", under("Q") + "uit"],
|
|
83
94
|
];
|
|
84
95
|
const buttonLine = buttons.map(([id, label]) => (id === "start" ? bold(cyan(`[ ${label} ]`)) : dim(`[ ${label} ]`))).join(" ");
|
|
85
|
-
const
|
|
96
|
+
const settings = [protoLine, portsLine, ...(SPEED_TEST ? [speedLine] : []), fwLine, randomLine, timeoutLine, dockerLine];
|
|
97
|
+
const table = grid([{ title: "" }, { title: "Code" }, { title: "Country" }], body, { caption: search, captionRight: dim(range), minWidth: Math.max(...settings.map(vw)) + 4 });
|
|
86
98
|
const width = vw(table[0]);
|
|
87
|
-
const head = box(
|
|
99
|
+
const head = box(settings, {
|
|
88
100
|
title: bold(cyan("WindScout")),
|
|
89
101
|
right: `${bold(String(selected.size))}${dim(`/${COUNTRIES.length} selected`)}`,
|
|
90
102
|
width: width - 4,
|
|
@@ -96,7 +108,7 @@ export function pick(saved, { timeout, speedTest, firewall, random, docker }) {
|
|
|
96
108
|
centerIn(buttonLine, width),
|
|
97
109
|
"",
|
|
98
110
|
centerIn(n
|
|
99
|
-
? `${bold(String(n))} attempts · estimated ${bold("≈ " + duration(estimate(inContainers, timeout, slots) + estimate(n - inContainers, timeout)))} ${dim(`(~${attemptSeconds(timeout)}s each${slots > 1 ? `, ${slots} at a time` : ""})`)}`
|
|
111
|
+
? `${bold(String(n))} attempts · estimated ${bold("≈ " + duration(login + estimate(inContainers, timeout, slots) + estimate(n - inContainers, timeout)))} ${dim(`(~${attemptSeconds(timeout)}s each${slots > 1 ? `, ${slots} at a time` : ""}${login ? `, + ~${duration(login)} login` : ""})`)}`
|
|
100
112
|
: yellow("select at least one country and one protocol"), width),
|
|
101
113
|
centerIn(dim(editing
|
|
102
114
|
? "type the seconds to wait for each connection · enter/esc done"
|
|
@@ -104,13 +116,14 @@ export function pick(saved, { timeout, speedTest, firewall, random, docker }) {
|
|
|
104
116
|
? "type to filter · ↑↓ move · enter/esc done"
|
|
105
117
|
// both lines fit 80 columns
|
|
106
118
|
: `↑↓ move · space select · a/n ${query ? "all/none shown · esc clear" : "all/none · / search"} · enter start · q quit`), width),
|
|
107
|
-
centerIn(dim(searching || editing ? "" : `1-${PROTOCOLS.length} protocols · s speed
|
|
119
|
+
centerIn(dim(searching || editing ? "" : `1-${PROTOCOLS.length} protocols · p ports${SPEED_TEST ? " · s speed" : ""} · f firewall · r random · t timeout · d docker`), width),
|
|
108
120
|
];
|
|
109
121
|
if (message)
|
|
110
122
|
lines.push(centerIn(yellow(message), width));
|
|
111
123
|
lines.push("", ...version.map((l) => centerIn(l, width)), centerIn(dim(repoLink), width));
|
|
112
124
|
const at = draw(lines);
|
|
113
125
|
// click map, in screen coordinates
|
|
126
|
+
const row = (line) => at.top + 1 + settings.indexOf(line); // a settings line, under the box's top border
|
|
114
127
|
let px = at.left + 2; // "│ " before the protocol line
|
|
115
128
|
const protoHits = PROTOCOLS.map((p, i) => {
|
|
116
129
|
const len = vw(`${i + 1} ● ${p}`);
|
|
@@ -126,7 +139,8 @@ export function pick(saved, { timeout, speedTest, firewall, random, docker }) {
|
|
|
126
139
|
return range;
|
|
127
140
|
});
|
|
128
141
|
hit = {
|
|
129
|
-
protoRow:
|
|
142
|
+
protoRow: row(protoLine), protoHits, portsRow: row(portsLine), speedRow: SPEED_TEST ? row(speedLine) : -1,
|
|
143
|
+
firewallRow: row(fwLine), randomRow: row(randomLine), timeoutRow: row(timeoutLine), dockerRow: row(dockerLine),
|
|
130
144
|
searchRow: at.top + head.length + 1,
|
|
131
145
|
firstRow: at.top + head.length + 5, shown: shown.length,
|
|
132
146
|
buttonRow: at.top + head.length + table.length + 1, buttonHits,
|
|
@@ -136,6 +150,7 @@ export function pick(saved, { timeout, speedTest, firewall, random, docker }) {
|
|
|
136
150
|
checkUpdate().then(() => !closed && render()); // the check usually lands after the first draw
|
|
137
151
|
if (docker)
|
|
138
152
|
dockerState().then((s) => ((dockerAt = s), !closed && render()));
|
|
153
|
+
protocolPorts().then((l) => ((portLists = l), !closed && render()));
|
|
139
154
|
const finish = (value) => {
|
|
140
155
|
closed = true;
|
|
141
156
|
out.off("resize", render);
|
|
@@ -161,6 +176,7 @@ export function pick(saved, { timeout, speedTest, firewall, random, docker }) {
|
|
|
161
176
|
speed: speedTest,
|
|
162
177
|
firewall,
|
|
163
178
|
random,
|
|
179
|
+
allPorts,
|
|
164
180
|
timeout,
|
|
165
181
|
docker,
|
|
166
182
|
};
|
|
@@ -214,6 +230,8 @@ export function pick(saved, { timeout, speedTest, firewall, random, docker }) {
|
|
|
214
230
|
if (i >= 0)
|
|
215
231
|
flip(protos, PROTOCOLS[i]);
|
|
216
232
|
}
|
|
233
|
+
else if (y === hit.portsRow)
|
|
234
|
+
allPorts = !allPorts;
|
|
217
235
|
else if (y === hit.speedRow)
|
|
218
236
|
speedTest = !speedTest;
|
|
219
237
|
else if (y === hit.firewallRow)
|
|
@@ -294,7 +312,9 @@ export function pick(saved, { timeout, speedTest, firewall, random, docker }) {
|
|
|
294
312
|
matches.forEach((c) => selected.add(c));
|
|
295
313
|
else if (k === "n")
|
|
296
314
|
matches.forEach((c) => selected.delete(c));
|
|
297
|
-
else if (k === "
|
|
315
|
+
else if (k === "p")
|
|
316
|
+
allPorts = !allPorts;
|
|
317
|
+
else if (k === "s" && SPEED_TEST)
|
|
298
318
|
speedTest = !speedTest;
|
|
299
319
|
else if (k === "f")
|
|
300
320
|
firewall = !firewall;
|
package/dist/tui/progress.js
CHANGED
|
@@ -24,7 +24,9 @@ export function showProgress(scan, blocking) {
|
|
|
24
24
|
const eta = (total - done) * (done ? elapsed / done : attemptSeconds(timeout) / slots.length);
|
|
25
25
|
const spinner = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"[frame++ % 10];
|
|
26
26
|
const ok = sortOk(results);
|
|
27
|
-
|
|
27
|
+
// blocked: nothing worked, and something was really tried (a key limit or a port the app doesn't offer tries nothing)
|
|
28
|
+
const failed = finished.filter((c) => !results.some((r) => r.query === c && r.status === "ok")
|
|
29
|
+
&& results.some((r) => r.query === c && r.status !== "key limit" && r.status !== "no such port"));
|
|
28
30
|
const attempt = ({ current: c }) => (c ? `${flag(c.country)} ${countryName(c.country)} ${dim("·")} ${c.protocol}` : "");
|
|
29
31
|
const many = slots.length > 1; // a parallel scan: a line per container
|
|
30
32
|
const pct = bold(`${String(Math.floor(fraction * 100)).padStart(3)}%`);
|
package/dist/tui/report.js
CHANGED
|
@@ -30,6 +30,15 @@ export function report(results, protocols, { all, speedTest }) {
|
|
|
30
30
|
console.log();
|
|
31
31
|
if (avail.length)
|
|
32
32
|
console.log(avail.join("\n"));
|
|
33
|
+
const limited = results.filter((r) => r.status === "key limit");
|
|
34
|
+
if (limited.length) {
|
|
35
|
+
const many = limited.length > 1, list = (xs) => [...new Set(xs)].join(",");
|
|
36
|
+
console.log([
|
|
37
|
+
yellow(`⚠ ${limited.length} WireGuard attempt${many ? "s" : ""} hit your Windscribe account's limit of WireGuard keys`),
|
|
38
|
+
dim(` (each docker container registers a key of its own), so ${many ? "these don't" : "it doesn't"} show whether WireGuard works from here.`),
|
|
39
|
+
` To find out, scan ${many ? "them" : "it"} on this computer: ${cyan(`windscout -c "${list(limited.map((r) => r.query))}" -p ${list(limited.map((r) => r.protocol))}`)}`,
|
|
40
|
+
].join("\n"));
|
|
41
|
+
}
|
|
33
42
|
if (!ok.length) {
|
|
34
43
|
console.log(box([yellow("Nothing connected."),
|
|
35
44
|
dim("Try other countries, ports (e.g. -p stealth:443,tcp:443) or a longer --timeout.")], { title: bold(cyan("WindScout")), width: width - 4 }).join("\n"));
|
|
@@ -37,7 +46,7 @@ export function report(results, protocols, { all, speedTest }) {
|
|
|
37
46
|
}
|
|
38
47
|
console.log(main.join("\n"));
|
|
39
48
|
const stats = protocols.map((p) => {
|
|
40
|
-
const tried = results.filter((r) => r.protocol === p &&
|
|
49
|
+
const tried = results.filter((r) => r.protocol === p && !["no such location", "key limit", "no such port"].includes(r.status));
|
|
41
50
|
const good = tried.filter((r) => r.status === "ok");
|
|
42
51
|
return { p, good: good.length, tried: tried.length, best: sortOk(good).at(0) };
|
|
43
52
|
}).sort((x, y) => y.good - x.good || (y.best?.speedMbps ?? 0) - (x.best?.speedMbps ?? 0) || (x.best?.latencyMs ?? 1e9) - (y.best?.latencyMs ?? 1e9));
|
package/dist/tui/tables.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { countryName, flag } from "./country.js";
|
|
2
|
-
import { bold, dim, fmt, green, red } from "./text.js";
|
|
2
|
+
import { bold, dim, fmt, green, red, yellow } from "./text.js";
|
|
3
3
|
import { grid } from "./widgets.js";
|
|
4
4
|
const RESULT_COLS = [
|
|
5
5
|
{ title: "#", align: "right" }, { title: "Country" }, { title: "Location" }, { title: "Protocol" },
|
|
@@ -14,15 +14,27 @@ const resultRow = (r, i) => [
|
|
|
14
14
|
r.connectS === null ? "-" : `${fmt(r.connectS, 1)} s`,
|
|
15
15
|
r.latencyMs === null ? "-" : `${fmt(r.latencyMs, 0)} ms`,
|
|
16
16
|
r.speedMbps === null ? "-" : bold(`${fmt(r.speedMbps, 1)} Mbps`),
|
|
17
|
-
r.status === "ok" ? green("✓ ok") : red(r.status),
|
|
17
|
+
r.status === "ok" ? green("✓ ok") : r.status === "key limit" || r.status === "no such port" ? yellow(r.status) : red(r.status),
|
|
18
18
|
];
|
|
19
19
|
/** Numbered attempts, in the given order; the Speed column only when the speed test ran */
|
|
20
20
|
export function resultTable(results, speedTest, options) {
|
|
21
21
|
const keep = (_, i) => speedTest || i !== SPEED_COL;
|
|
22
22
|
return grid(RESULT_COLS.filter(keep), results.map((r, i) => resultRow(r, i).filter(keep)), options);
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
const base = (protocol) => protocol.split(":")[0];
|
|
25
|
+
/** A protocol's cell: ✓/✗, with how many of its ports worked when it has several (✓ 3/11); · = not tried yet,
|
|
26
|
+
* key limit = couldn't try WireGuard, no port = the app doesn't offer it. Those last two don't count */
|
|
27
|
+
function cell(rs, ports) {
|
|
28
|
+
const tried = rs.filter((r) => ports.includes(r.protocol));
|
|
29
|
+
const real = tried.filter((r) => r.status !== "key limit" && r.status !== "no such port");
|
|
30
|
+
const good = real.filter((r) => r.status === "ok").length, count = ports.length > 1 ? ` ${good}/${real.length}` : "";
|
|
31
|
+
if (real.length)
|
|
32
|
+
return good ? green(`✓${count}`) : red(`✗${count}`);
|
|
33
|
+
return tried.some((r) => r.status === "key limit") ? yellow("key limit") : tried.length ? yellow("no port") : dim("·");
|
|
34
|
+
}
|
|
35
|
+
/** One row per country, a cell per protocol (its ports share it); no lines until something was tried */
|
|
25
36
|
export function matrix(results, protocols, { minWidth = 0, limit = Infinity } = {}) {
|
|
37
|
+
const columns = [...new Set(protocols.map(base))].map((b) => protocols.filter((p) => base(p) === b));
|
|
26
38
|
const byCountry = new Map();
|
|
27
39
|
for (const r of results) {
|
|
28
40
|
if (r.status === "no such location" || r.status === "interrupted")
|
|
@@ -45,15 +57,13 @@ export function matrix(results, protocols, { minWidth = 0, limit = Infinity } =
|
|
|
45
57
|
[
|
|
46
58
|
`${flag(q)} ${countryName(q)}`,
|
|
47
59
|
first,
|
|
48
|
-
...
|
|
49
|
-
const r = rs.find((x) => x.protocol === p);
|
|
50
|
-
return !r ? dim("·") : r.status === "ok" ? green("✓") : red("✗");
|
|
51
|
-
}),
|
|
60
|
+
...columns.map((ports) => cell(rs, ports)),
|
|
52
61
|
],
|
|
53
|
-
...more.map((place) => ["", place, ...
|
|
62
|
+
...more.map((place) => ["", place, ...columns.map(() => "")]),
|
|
54
63
|
];
|
|
55
64
|
}).slice(0, limit);
|
|
56
|
-
return grid([{ title: "Country" }, { title: "Location(s)" },
|
|
65
|
+
return grid([{ title: "Country" }, { title: "Location(s)" },
|
|
66
|
+
...columns.map((ports) => ({ title: ports.length > 1 ? base(ports[0]) : ports[0], align: "center" }))], rows, {
|
|
57
67
|
caption: bold("Availability"),
|
|
58
68
|
captionRight: dim(`${reachable}/${ranked.length} countries reachable${ranked.length > limit ? ` · top ${limit}` : ""}`),
|
|
59
69
|
minWidth,
|
package/dist/windscribe.js
CHANGED
|
@@ -15,6 +15,19 @@ const DESKTOP_CLI = {
|
|
|
15
15
|
let cliBin = CLI;
|
|
16
16
|
// IKEv2 is offered by the macOS/Windows apps only
|
|
17
17
|
export const PROTOCOLS = ["wireguard", ...(process.platform === "linux" ? [] : ["ikev2"]), "stealth", "wstunnel", "udp", "tcp"];
|
|
18
|
+
let portLists;
|
|
19
|
+
/** The ports the app offers for each protocol, its default first; [] where it lists none (IKEv2 on Linux, or apps before 2.22,
|
|
20
|
+
* which have no `ports`). Asked once, one protocol at a time: windscribe-cli runs one at a time */
|
|
21
|
+
export const protocolPorts = () => (portLists ??= (async () => {
|
|
22
|
+
const lists = new Map();
|
|
23
|
+
for (const p of PROTOCOLS) {
|
|
24
|
+
const out = await windscribe("ports", p).catch(() => ""); // can't ask: no list, so the default port
|
|
25
|
+
lists.set(p, out.match(/^\d+(, \d+)*$/m)?.[0].split(", ").map(Number) ?? []);
|
|
26
|
+
}
|
|
27
|
+
return lists;
|
|
28
|
+
})());
|
|
29
|
+
/** Each protocol given without a port, on every port in lists: "tcp" → "tcp:443", "tcp:587", …; the rest as given */
|
|
30
|
+
export const withPorts = (protocols, lists) => protocols.flatMap((p) => (!p.includes(":") && lists.get(p)?.length ? lists.get(p).map((port) => `${p}:${port}`) : [p]));
|
|
18
31
|
// ponytail: hardcoded because `windscribe-cli locations` returns nothing on 2.24; refresh when Windscribe adds countries
|
|
19
32
|
export const COUNTRIES = `US CA MX BR AR CL CO PE GB IE FR DE NL BE LU CH AT IT ES PT DK NO SE FI IS
|
|
20
33
|
PL CZ SK HU RO BG GR CY TR RS HR SI BA MK AL MD UA EE LV LT IL AE AZ GE AM KZ IN JP KR
|
package/dist/worker.js
CHANGED
|
@@ -41,9 +41,10 @@ export async function worker() {
|
|
|
41
41
|
writeFileSync(SETTINGS, withSession(ini, session));
|
|
42
42
|
// no systemd in a container: start the app like its unit would, as this user (its CLI refuses root)
|
|
43
43
|
spawn(APP, { stdio: "ignore" }).on("error", () => { }); // not there: its CLI never answers below
|
|
44
|
-
// its CLI answers once it's up, and says logged in once Windscribe accepts the session, or the error it got instead
|
|
44
|
+
// its CLI answers once it's up, and says logged in once Windscribe accepts the session, or the error it got instead.
|
|
45
|
+
// One container took 37 s in a test, 16 at once 38-75 s
|
|
45
46
|
let status = "";
|
|
46
|
-
for (const t = now(); now() - t <
|
|
47
|
+
for (const t = now(); now() - t < 180 && !/Login state: (Logged in|Error)/.test(status = await windscribe("status"));)
|
|
47
48
|
await sleep(1000);
|
|
48
49
|
if (!status.includes("Logged in")) {
|
|
49
50
|
const state = status.match(/^Login state: (.*)$/m)?.[1].trim();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "windscout",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"description": "Find which Windscribe locations and protocols work from your network, ranked by
|
|
3
|
+
"version": "0.6.4",
|
|
4
|
+
"description": "Find which Windscribe locations and protocols work from your network, ranked by latency (unofficial)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"windscout": "dist/cli.js"
|
|
@@ -25,14 +25,12 @@
|
|
|
25
25
|
"keywords": [
|
|
26
26
|
"windscribe",
|
|
27
27
|
"vpn",
|
|
28
|
-
"speedtest",
|
|
29
28
|
"wireguard",
|
|
30
29
|
"stealth",
|
|
31
30
|
"cli",
|
|
32
31
|
"censorship",
|
|
33
32
|
"firewall",
|
|
34
33
|
"unblock",
|
|
35
|
-
"vpn-speed-test",
|
|
36
34
|
"server-finder",
|
|
37
35
|
"docker"
|
|
38
36
|
],
|