windscout 0.4.3 → 0.4.5

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 (3) hide show
  1. package/README.md +7 -1
  2. package/package.json +1 -1
  3. package/windscout.js +46 -13
package/README.md CHANGED
@@ -51,6 +51,7 @@ Run `windscout` with no arguments to open the interactive picker:
51
51
  | `a` / `n` | select all / unselect all (only the matching countries while a search is active) |
52
52
  | `1`–`6` | toggle a protocol (`1`–`5` on Linux, which has no IKEv2) |
53
53
  | `s` | toggle the speed test (off = connect-only mode) |
54
+ | `f` | toggle "block internet while scanning" (firewall) |
54
55
  | `enter` | start scanning |
55
56
  | `q` | quit (during a scan: stop and show partial results; press again to force quit) |
56
57
 
@@ -77,6 +78,7 @@ windscout -t 40 -a # longer connect timeout, also list
77
78
  | `-t, --timeout` | seconds to wait for each connection | 20 |
78
79
  | `-a, --all` | include failed attempts in the final table | off |
79
80
  | `-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 |
81
+ | `-f, --firewall` | block internet while scanning, except through the VPN | picker: saved, or off; with `-c`: off |
80
82
  | `-v, --version` | print the installed version, and the update command if a newer one is on npm | |
81
83
 
82
84
  ## Connect-only mode
@@ -98,7 +100,11 @@ Both modes show an **availability grid**: one row per country, and ✓ / ✗ for
98
100
  ╰─────────────────────────┴────────────────────────┴─────────┴───────────┴─────╯
99
101
  ```
100
102
 
101
- While scanning, press `tab` to switch between the availability grid and the speed or latency ranking. In connect-only mode the scan starts on the availability grid.
103
+ While scanning, press `tab` to switch between the availability grid and the speed or 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. In connect-only mode the scan starts on the availability grid.
104
+
105
+ ## Block internet while scanning (firewall)
106
+
107
+ Between attempts the VPN is disconnected for a moment, and apps on your computer (browser, messengers, updates) can go online over your real connection. Turn on **block internet while scanning** with `f` in the picker or `--firewall`, and WindScout switches on the Windscribe app's firewall for the scan: nothing gets online except through the VPN. When the scan ends or you stop it, the firewall goes back off (if you already had it on, WindScout leaves it on). While a scan is paused, the firewall keeps blocking.
102
108
 
103
109
  ## Output
104
110
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "windscout",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Find which Windscribe locations and protocols work from your network, ranked by speed (unofficial)",
5
5
  "type": "module",
6
6
  "bin": {
package/windscout.js CHANGED
@@ -47,6 +47,8 @@ const noFlags = process.platform === "win32" || process.env.TERM_PROGRAM === "Ap
47
47
  const flag = (code) => !noFlags && /^[A-Z]{2}$/.test(code) ? String.fromCodePoint(...[...code].map((c) => 0x1f1a5 + c.charCodeAt(0))) : " ";
48
48
  let stop = 0;
49
49
  let speedTest = true; // false = connect-only mode: skip the download test, rank by latency
50
+ let firewall = false; // turn on the app's firewall while scanning, so nothing goes online outside the VPN
51
+ let restoreFirewall = async () => {}; // set by scan(); also called on a crash so the internet isn't left blocked
50
52
 
51
53
  // ---------- windscribe + measurements ----------
52
54
 
@@ -314,6 +316,7 @@ function loadConfig() {
314
316
  countries: (c.countries ?? []).filter((x) => COUNTRIES.includes(x)),
315
317
  protocols: (c.protocols ?? []).filter((x) => PROTOCOLS.includes(x)),
316
318
  speed: c.speed !== false,
319
+ firewall: c.firewall === true,
317
320
  };
318
321
  } catch {
319
322
  return null; // first run or unreadable file
@@ -415,7 +418,7 @@ function pick(saved, timeout) {
415
418
  const render = () => {
416
419
  const rows = out.rows || 24;
417
420
  const list = visible();
418
- const height = Math.max(3, rows - 19 - (update ? 1 : 0));
421
+ const height = Math.max(3, rows - 20 - (update ? 1 : 0));
419
422
  cursor = Math.max(0, Math.min(cursor, list.length - 1));
420
423
  if (cursor < top) top = cursor;
421
424
  if (cursor >= top + height) top = cursor - height + 1;
@@ -426,6 +429,10 @@ function pick(saved, timeout) {
426
429
  const speedLine = `${dim(under("s"))} ${speedTest ? green("● speed test") : dim("○ speed test")} ${dim(speedTest
427
430
  ? "ranks by download speed · adds ~15s per working connection"
428
431
  : "off: connect-only, ranks by latency")}`;
432
+ const fwLabel = "block internet while scanning (firewall)";
433
+ const fwLine = `${dim(under("f"))} ${firewall ? green("● " + fwLabel) : dim("○ " + fwLabel)} ${dim(firewall
434
+ ? "only VPN traffic gets through"
435
+ : "off: apps may leak in between")}`;
429
436
  const search = searching
430
437
  ? `${cyan("🔍")} ${bold(query)}${cyan("▏")}`
431
438
  : query ? `🔍 ${query}` : dim("🔍 press / to search");
@@ -442,9 +449,9 @@ function pick(saved, timeout) {
442
449
  const buttonLine = buttons.map(([id, label]) => (id === "start" ? bold(cyan(`[ ${label} ]`)) : dim(`[ ${label} ]`))).join(" ");
443
450
 
444
451
  const table = grid([{ title: "" }, { title: "Code" }, { title: "Country" }], body,
445
- { caption: search, captionRight: dim(range), minWidth: Math.max(vw(protoLine), vw(speedLine)) + 4 });
452
+ { caption: search, captionRight: dim(range), minWidth: Math.max(vw(protoLine), vw(speedLine), vw(fwLine)) + 4 });
446
453
  const width = vw(table[0]);
447
- const head = box([protoLine, speedLine], {
454
+ const head = box([protoLine, speedLine, fwLine], {
448
455
  title: bold(cyan("WindScout")),
449
456
  right: `${bold(String(selected.size))}${dim(`/${COUNTRIES.length} selected`)}`,
450
457
  width: width - 4,
@@ -460,7 +467,7 @@ function pick(saved, timeout) {
460
467
  centerIn(dim(searching
461
468
  ? "type to filter · ↑↓ move · enter/esc done"
462
469
  : `↑↓ move · space select · a/n ${query ? "all/none shown" : "all/none"} · / search`), width),
463
- centerIn(dim(searching ? "" : `1-${PROTOCOLS.length} protocols · s speed test · enter start · ${query ? "esc clear · " : ""}q quit`), width),
470
+ centerIn(dim(searching ? "" : `1-${PROTOCOLS.length} protocols · s speed test · f firewall · enter start · ${query ? "esc clear · " : ""}q quit`), width),
464
471
  ];
465
472
  lines.splice(head.length + table.length + 1, 0, centerIn(buttonLine, width), "");
466
473
  if (message) lines.push(centerIn(yellow(message), width));
@@ -485,7 +492,7 @@ function pick(saved, timeout) {
485
492
  return range;
486
493
  });
487
494
  hit = {
488
- protoRow: y0 + 1, speedRow: y0 + 2, protoHits,
495
+ protoRow: y0 + 1, speedRow: y0 + 2, firewallRow: y0 + 3, protoHits,
489
496
  searchRow: y0 + head.length + 1,
490
497
  firstRow: y0 + head.length + 5, shown: shown.length,
491
498
  buttonRow: y0 + head.length + table.length + 1, buttonHits,
@@ -510,6 +517,7 @@ function pick(saved, timeout) {
510
517
  countries: COUNTRIES.filter((c) => selected.has(c)),
511
518
  protocols: PROTOCOLS.filter((p) => protos.has(p)),
512
519
  speed: speedTest,
520
+ firewall,
513
521
  };
514
522
  saveConfig(choice);
515
523
  finish(choice);
@@ -535,6 +543,7 @@ function pick(saved, timeout) {
535
543
  const i = hit.protoHits.findIndex(([a, b]) => x >= a && x < b);
536
544
  if (i >= 0) protos.has(PROTOCOLS[i]) ? protos.delete(PROTOCOLS[i]) : protos.add(PROTOCOLS[i]);
537
545
  } else if (y === hit.speedRow) speedTest = !speedTest;
546
+ else if (y === hit.firewallRow) firewall = !firewall;
538
547
  else if (y === hit.buttonRow) {
539
548
  const id = hit.buttonHits.find(([, a, b]) => x >= a && x < b)?.[0];
540
549
  if (id === "start" && start()) return;
@@ -578,6 +587,7 @@ function pick(saved, timeout) {
578
587
  else if (k === "a") list.forEach((c) => selected.add(c));
579
588
  else if (k === "n") list.forEach((c) => selected.delete(c));
580
589
  else if (k === "s") speedTest = !speedTest;
590
+ else if (k === "f") firewall = !firewall;
581
591
  else if (/^[1-9]$/.test(str ?? "") && PROTOCOLS[Number(str) - 1]) {
582
592
  const p = PROTOCOLS[Number(str) - 1];
583
593
  protos.has(p) ? protos.delete(p) : protos.add(p);
@@ -599,12 +609,18 @@ async function scan(countries, protocols, timeout, tty) {
599
609
  const started = now();
600
610
  let done = 0, attemptStart = now(), current = "", step = "", frame = 0, finishedCountries = [];
601
611
  let view = speedTest ? "ranking" : "availability"; // tab switches
612
+ // pause lands between attempts, so the VPN is disconnected while waiting; idle = seconds spent paused
613
+ let paused = false, waiting = false, waitStart = 0, idle = 0, resume = () => {};
614
+ // firewall: block everything outside the VPN while switching locations; put it back the way the user had it
615
+ const fwChanged = firewall && /^Firewall state: Off/m.test(await cli("status"));
616
+ if (fwChanged) await cli("firewall", "on");
617
+ restoreFirewall = async () => { if (fwChanged) await cli("firewall", "off"); };
602
618
 
603
619
  const render = () => {
604
620
  const rows = out.rows || 24;
605
- const elapsed = now() - started;
621
+ const elapsed = now() - started - idle - (waiting ? now() - waitStart : 0);
606
622
  // smooth bar: count the running attempt as partially done
607
- const partial = Math.min(0.95, (now() - attemptStart) / attemptSeconds(timeout));
623
+ const partial = waiting ? 0 : Math.min(0.95, (now() - attemptStart) / attemptSeconds(timeout));
608
624
  const fraction = (done + (done < total ? partial : 0)) / total;
609
625
  const eta = (total - done) * (done ? elapsed / done : attemptSeconds(timeout));
610
626
  const spinner = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"[frame++ % 10];
@@ -640,8 +656,9 @@ async function scan(countries, protocols, timeout, tty) {
640
656
  "",
641
657
  stop
642
658
  ? yellow(" stopping after the current attempt…")
659
+ : waiting ? yellow(` paused · VPN disconnected${fwChanged ? " · firewall blocking internet" : ""} · press p to resume`)
643
660
  : ` ${cyan(spinner)} ${bold(current)}`,
644
- stop ? "" : ` ${dim(step)} ${dim("·")} ${duration(now() - attemptStart)}`,
661
+ stop || waiting ? "" : ` ${dim(step)} ${dim("·")} ${duration(now() - attemptStart)}${paused && frame % 8 < 5 ? yellow(" · pausing after this attempt") : ""}`,
645
662
  null,
646
663
  centerIn(stats, inner),
647
664
  ], {
@@ -658,34 +675,45 @@ async function scan(countries, protocols, timeout, tty) {
658
675
  while (vw(txt) > width && failed.length) txt = txt.slice(0, -8) + "…"; // ponytail: crude trim, fine for a status line
659
676
  lines.push("", centerIn(txt, width));
660
677
  }
661
- lines.push("", centerIn(dim(`tab ${view === "ranking" ? "availability" : "ranking"} view · q stop · q twice force quit`), width), "", centerIn(dim(repoLink), width));
678
+ lines.push("", centerIn(dim(`tab ${view === "ranking" ? "availability" : "ranking"} view · p ${paused ? "resume" : "pause"} · q stop · q twice force quit`), width), "", centerIn(dim(repoLink), width));
662
679
  draw(lines);
663
680
  };
664
681
 
665
682
  let timer;
666
683
  if (tty) {
667
684
  if (!fullscreen) enterScreen();
668
- input.on("keypress", (str, key = {}) => {
685
+ input.on("keypress", async (str, key = {}) => {
669
686
  if (key.name === "tab") {
670
687
  view = view === "ranking" ? "availability" : "ranking";
671
688
  render();
689
+ } else if (key.name === "p" || key.name === "space") {
690
+ paused = !paused;
691
+ if (!paused) resume();
692
+ render();
672
693
  } else if (key.name === "q" || key.name === "escape" || (key.ctrl && key.name === "c")) {
673
694
  if (stop++) { // second press: bail out now
674
695
  leaveScreen();
675
696
  console.error(`stopped. the VPN may still be connecting: run '${CLI} disconnect'`);
697
+ await restoreFirewall();
676
698
  process.exit(130);
677
699
  }
700
+ resume(); // a paused scan stops right away
678
701
  render();
679
702
  }
680
703
  });
681
704
  out.on("resize", render);
682
705
  timer = setInterval(render, 120);
683
706
  } else {
684
- process.on("SIGINT", () => { if (stop++) process.exit(130); });
707
+ process.on("SIGINT", async () => { if (stop++) await restoreFirewall(), process.exit(130); });
685
708
  }
686
709
 
687
710
  outer: for (const loc of countries) {
688
711
  for (const proto of protocols) {
712
+ if (paused && !stop) {
713
+ waiting = true, waitStart = now();
714
+ await new Promise((r) => (resume = r));
715
+ waiting = false, idle += now() - waitStart;
716
+ }
689
717
  if (stop) break outer;
690
718
  current = `${flag(loc)} ${countryName(loc)} ${dim("·")} ${proto}`;
691
719
  attemptStart = now();
@@ -698,6 +726,7 @@ async function scan(countries, protocols, timeout, tty) {
698
726
  }
699
727
  finishedCountries.push(loc); // table/failed list refresh per finished country
700
728
  }
729
+ await restoreFirewall();
701
730
 
702
731
  if (tty) {
703
732
  clearInterval(timer);
@@ -767,6 +796,7 @@ Options:
767
796
  -t, --timeout SEC seconds to wait for each connection (default: 20)
768
797
  -a, --all also show failed attempts in the final table
769
798
  -n, --no-speed connect-only: skip the download test, rank by latency (faster scan)
799
+ -f, --firewall block internet while scanning, except through the VPN (turned back off afterwards)
770
800
  -v, --version show the installed version (and whether a newer one is out)
771
801
  -h, --help show this help
772
802
 
@@ -783,6 +813,7 @@ async function main() {
783
813
  help: { type: "boolean", short: "h", default: false },
784
814
  version: { type: "boolean", short: "v", default: false },
785
815
  "no-speed": { type: "boolean", short: "n", default: false },
816
+ firewall: { type: "boolean", short: "f", default: false },
786
817
  },
787
818
  });
788
819
  if (a.help) return console.log(HELP);
@@ -810,13 +841,14 @@ async function main() {
810
841
  const saved = loadConfig();
811
842
  if (protocols && saved) saved.protocols = protocols.filter((p) => PROTOCOLS.includes(p));
812
843
  speedTest = !a["no-speed"] && (saved?.speed ?? false); // first run: connect-only
844
+ firewall = a.firewall || (saved?.firewall ?? false);
813
845
  const choice = await pick(saved, timeout);
814
846
  if (!choice) return;
815
847
  countries = choice.countries;
816
848
  protocols = protocols ?? choice.protocols;
817
849
  }
818
850
  protocols ??= PROTOCOLS;
819
- if (a.countries) speedTest = !a["no-speed"];
851
+ if (a.countries) speedTest = !a["no-speed"], firewall = a.firewall;
820
852
 
821
853
  const results = await scan(countries, protocols, timeout, tty);
822
854
  if (stop) console.log(yellow("interrupted, showing partial results"));
@@ -825,8 +857,9 @@ async function main() {
825
857
  if (update) console.error("\n" + yellow(updateNotice()));
826
858
  }
827
859
 
828
- main().catch((e) => {
860
+ main().catch(async (e) => {
829
861
  leaveScreen();
830
862
  console.error(`windscout: ${e.message}`);
863
+ await restoreFirewall().catch(() => console.error(`the firewall may still be on: run '${CLI} firewall off'`));
831
864
  process.exit(1);
832
865
  });