killm 1.0.0 → 1.0.3
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 +79 -5
- package/dist/src/cli.js +7 -0
- package/dist/src/firewall.js +175 -0
- package/dist/src/hosts.js +48 -0
- package/dist/src/index.js +79 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -39,6 +39,36 @@ Editing the hosts file needs elevated privileges:
|
|
|
39
39
|
- **macOS / Linux:** prefix with `sudo` → `sudo npx killm for 1h --agents`
|
|
40
40
|
- **Windows:** run from an **Administrator** terminal
|
|
41
41
|
|
|
42
|
+
### WSL users — read this
|
|
43
|
+
|
|
44
|
+
WSL and Windows have **separate hosts files**. Running `sudo npx killm` inside
|
|
45
|
+
WSL edits WSL's `/etc/hosts`, which only blocks processes running _inside_ WSL
|
|
46
|
+
(curl, node, coding agents in your WSL shell). Your browser is a Windows app —
|
|
47
|
+
it reads `C:\Windows\System32\drivers\etc\hosts` and is **not affected**.
|
|
48
|
+
|
|
49
|
+
To block Windows apps (the browser, Windows-side editors), run killm **on
|
|
50
|
+
Windows** from an Administrator PowerShell:
|
|
51
|
+
|
|
52
|
+
```powershell
|
|
53
|
+
npx killm for 1h --web
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
killm detects WSL and prints a warning when this applies. If you want both
|
|
57
|
+
sides blocked, run it in both places.
|
|
58
|
+
|
|
59
|
+
### Browser caveats
|
|
60
|
+
|
|
61
|
+
Two browser behaviors can make a block look like it isn't working:
|
|
62
|
+
|
|
63
|
+
- **Secure DNS (DNS-over-HTTPS):** when enabled, the browser resolves names
|
|
64
|
+
through an encrypted remote resolver and **bypasses the hosts file
|
|
65
|
+
entirely**. Disable it (Chrome/Edge: Settings → Privacy → Security → "Use
|
|
66
|
+
secure DNS"; Firefox: Settings → Privacy → DNS over HTTPS) for the block to
|
|
67
|
+
apply.
|
|
68
|
+
- **Browser DNS caching:** already-open tabs and cached lookups keep working
|
|
69
|
+
for a while. Restart the browser, or clear its DNS cache
|
|
70
|
+
(`chrome://net-internals/#dns` in Chrome/Edge).
|
|
71
|
+
|
|
42
72
|
## The key idea: agents vs. web
|
|
43
73
|
|
|
44
74
|
The whole point of `killm` is that an agentic coding tool and a chat website
|
|
@@ -95,6 +125,7 @@ You can combine `--agents` and `--web`; that's the same as `--all`.
|
|
|
95
125
|
|
|
96
126
|
| Option | Effect |
|
|
97
127
|
| ----------------- | ------------------------------------------------ |
|
|
128
|
+
| `--firewall` | Also block current IPs at the OS firewall |
|
|
98
129
|
| `--restore` | Lift any active block right now and exit |
|
|
99
130
|
| `--status` | Report whether a block is currently active |
|
|
100
131
|
| `--list` | Print the hostnames a given scope would block |
|
|
@@ -132,12 +163,55 @@ target hostname at `0.0.0.0` (and `::1` for IPv6):
|
|
|
132
163
|
```
|
|
133
164
|
|
|
134
165
|
It then flushes the OS DNS cache so the change takes effect immediately. When
|
|
135
|
-
the timer expires
|
|
136
|
-
block back out and flushes again. The markers mean
|
|
137
|
-
lines — your existing hosts entries are left
|
|
166
|
+
the timer expires — or on `Ctrl+C`, `SIGTERM`, or closing the terminal
|
|
167
|
+
(`SIGHUP`) — it strips that block back out and flushes again. The markers mean
|
|
168
|
+
it only ever touches its own lines — your existing hosts entries are left
|
|
169
|
+
alone.
|
|
170
|
+
|
|
171
|
+
### If the killm process dies, the block stays — until killm heals it
|
|
172
|
+
|
|
173
|
+
The timer lives in the killm process, so **keep it running** for the duration.
|
|
174
|
+
Ctrl+C and closing the terminal both lift the block cleanly, but a `kill -9`,
|
|
175
|
+
a crash, or a machine shutdown can strand the block in your hosts file.
|
|
176
|
+
|
|
177
|
+
killm writes the expiry time into the block itself, so it self-heals: **any
|
|
178
|
+
later killm command** (`--status`, `--restore`, or starting a new block)
|
|
179
|
+
notices an expired stranded block and removes it. To lift one immediately:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
sudo npx killm --restore
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`killm --status` also tells you when an active block is scheduled to lift.
|
|
186
|
+
|
|
187
|
+
## Firewall mode (`--firewall`)
|
|
188
|
+
|
|
189
|
+
The hosts file only intercepts name resolution, so a browser with **Secure DNS
|
|
190
|
+
(DoH)** enabled bypasses it. `--firewall` closes that hole: in addition to the
|
|
191
|
+
hosts entries, killm resolves each target hostname to its **current IPs** and
|
|
192
|
+
blocks those at the OS firewall:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
sudo npx killm for 1h --web --firewall
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
- **Linux:** `iptables` / `ip6tables` OUTPUT rules, tagged with a `killm`
|
|
199
|
+
comment
|
|
200
|
+
- **Windows:** one outbound Windows Firewall rule named `killm` (via `netsh`)
|
|
201
|
+
- **macOS:** a `pf` anchor named `killm`
|
|
202
|
+
|
|
203
|
+
Rules are removed together with the hosts block (timer, `Ctrl+C`, or
|
|
204
|
+
`killm --restore` — which also sweeps up rules left behind by a crash, since
|
|
205
|
+
they're all tagged).
|
|
206
|
+
|
|
207
|
+
Caveats, honestly stated:
|
|
138
208
|
|
|
139
|
-
|
|
140
|
-
|
|
209
|
+
- IPs are captured **at block time**; if a provider rotates addresses
|
|
210
|
+
mid-block, new IPs aren't covered.
|
|
211
|
+
- Big providers sit behind shared CDNs — blocking their current IPs _may_
|
|
212
|
+
affect unrelated sites served from the same edge.
|
|
213
|
+
- Inside WSL, iptables rules (like the hosts file) only affect WSL traffic,
|
|
214
|
+
not Windows apps.
|
|
141
215
|
|
|
142
216
|
## Limitations & honesty
|
|
143
217
|
|
package/dist/src/cli.js
CHANGED
|
@@ -23,6 +23,9 @@ SCOPE (pick one or more; default is --all)
|
|
|
23
23
|
--all Block both of the above. This is the default if no scope is given.
|
|
24
24
|
|
|
25
25
|
OPTIONS
|
|
26
|
+
--firewall Also block the targets' current IPs at the OS firewall
|
|
27
|
+
(iptables/ip6tables, Windows Firewall, or pf). Catches
|
|
28
|
+
browsers that bypass the hosts file via Secure DNS (DoH).
|
|
26
29
|
--restore Remove any active killm block right now and exit.
|
|
27
30
|
--status Show whether a block is currently active and exit.
|
|
28
31
|
--list Print the hostnames that would be blocked and exit.
|
|
@@ -50,6 +53,7 @@ export function parseArgs(argv) {
|
|
|
50
53
|
scope: { agents: false, web: false, all: false },
|
|
51
54
|
dryRun: false,
|
|
52
55
|
yes: false,
|
|
56
|
+
firewall: false,
|
|
53
57
|
};
|
|
54
58
|
let explicit;
|
|
55
59
|
let durationInput;
|
|
@@ -74,6 +78,9 @@ export function parseArgs(argv) {
|
|
|
74
78
|
case '--dry-run':
|
|
75
79
|
result.dryRun = true;
|
|
76
80
|
break;
|
|
81
|
+
case '--firewall':
|
|
82
|
+
result.firewall = true;
|
|
83
|
+
break;
|
|
77
84
|
case '-y':
|
|
78
85
|
case '--yes':
|
|
79
86
|
result.yes = true;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import dns from 'node:dns/promises';
|
|
3
|
+
/**
|
|
4
|
+
* Firewall-level blocking, layered on top of the hosts-file block.
|
|
5
|
+
*
|
|
6
|
+
* The hosts file only intercepts name resolution, so a browser with Secure
|
|
7
|
+
* DNS (DoH) enabled sails right past it. Firewall mode resolves each target
|
|
8
|
+
* hostname to its current IPs and blocks those at the OS firewall:
|
|
9
|
+
*
|
|
10
|
+
* - Linux: iptables / ip6tables OUTPUT rules tagged with a "killm" comment
|
|
11
|
+
* - Windows: a single "killm" outbound netsh advfirewall rule
|
|
12
|
+
* - macOS: a pf anchor named "killm"
|
|
13
|
+
*
|
|
14
|
+
* All rules are identifiable without local state, so `killm --restore` can
|
|
15
|
+
* always clean up, even after a crash.
|
|
16
|
+
*/
|
|
17
|
+
export const RULE_TAG = 'killm';
|
|
18
|
+
/** Maximum IPs per netsh rule; Windows accepts a comma list but not unbounded. */
|
|
19
|
+
const WINDOWS_CHUNK = 100;
|
|
20
|
+
export const defaultRunner = (cmd, args, stdin) => new Promise((resolve) => {
|
|
21
|
+
const child = execFile(cmd, args, (error, stdout) => resolve({ ok: !error, stdout: stdout ?? '' }));
|
|
22
|
+
if (stdin != null && child.stdin) {
|
|
23
|
+
child.stdin.write(stdin);
|
|
24
|
+
child.stdin.end();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* Resolve every hostname to its current A/AAAA records. Failures are
|
|
29
|
+
* collected, not thrown — an unresolvable host simply can't be blocked by IP.
|
|
30
|
+
*/
|
|
31
|
+
export async function resolveTargetIPs(hosts, resolver = dns) {
|
|
32
|
+
const v4 = new Set();
|
|
33
|
+
const v6 = new Set();
|
|
34
|
+
const unresolved = [];
|
|
35
|
+
await Promise.all(hosts.map(async (host) => {
|
|
36
|
+
const [a, aaaa] = await Promise.all([
|
|
37
|
+
resolver.resolve4(host).catch(() => []),
|
|
38
|
+
resolver.resolve6(host).catch(() => []),
|
|
39
|
+
]);
|
|
40
|
+
if (a.length === 0 && aaaa.length === 0) {
|
|
41
|
+
unresolved.push(host);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
for (const ip of a)
|
|
45
|
+
v4.add(ip);
|
|
46
|
+
for (const ip of aaaa)
|
|
47
|
+
v6.add(ip);
|
|
48
|
+
}));
|
|
49
|
+
return { v4: Array.from(v4).sort(), v6: Array.from(v6).sort(), unresolved: unresolved.sort() };
|
|
50
|
+
}
|
|
51
|
+
// ---- pure command builders (unit-tested per platform) --------------------
|
|
52
|
+
/** iptables/ip6tables arguments to add one REJECT rule tagged with killm. */
|
|
53
|
+
export function linuxAddArgs(ip) {
|
|
54
|
+
return ['-I', 'OUTPUT', '-d', ip, '-j', 'REJECT', '-m', 'comment', '--comment', RULE_TAG];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Convert `iptables -S OUTPUT` output into the `-D` argument lists needed to
|
|
58
|
+
* delete every killm-tagged rule. Our rules never contain quoted spaces, so
|
|
59
|
+
* whitespace splitting is safe.
|
|
60
|
+
*/
|
|
61
|
+
export function linuxDeleteArgsFromListing(listing) {
|
|
62
|
+
const out = [];
|
|
63
|
+
for (const line of listing.split('\n')) {
|
|
64
|
+
const trimmed = line.trim();
|
|
65
|
+
if (!trimmed.startsWith('-A OUTPUT'))
|
|
66
|
+
continue;
|
|
67
|
+
if (!trimmed.includes(`--comment ${RULE_TAG}`))
|
|
68
|
+
continue;
|
|
69
|
+
out.push(['-D', ...trimmed.slice('-A '.length).split(/\s+/)]);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/** netsh argument lists to add the killm outbound block rule(s). */
|
|
74
|
+
export function windowsAddArgs(ips) {
|
|
75
|
+
const out = [];
|
|
76
|
+
for (let i = 0; i < ips.length; i += WINDOWS_CHUNK) {
|
|
77
|
+
const chunk = ips.slice(i, i + WINDOWS_CHUNK);
|
|
78
|
+
out.push([
|
|
79
|
+
'advfirewall',
|
|
80
|
+
'firewall',
|
|
81
|
+
'add',
|
|
82
|
+
'rule',
|
|
83
|
+
`name=${RULE_TAG}`,
|
|
84
|
+
'dir=out',
|
|
85
|
+
'action=block',
|
|
86
|
+
`remoteip=${chunk.join(',')}`,
|
|
87
|
+
]);
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
/** netsh argument list that deletes every rule named killm. */
|
|
92
|
+
export function windowsDeleteArgs() {
|
|
93
|
+
return ['advfirewall', 'firewall', 'delete', 'rule', `name=${RULE_TAG}`];
|
|
94
|
+
}
|
|
95
|
+
/** pf rule text for the killm anchor on macOS. */
|
|
96
|
+
export function darwinAnchorRules(v4, v6) {
|
|
97
|
+
const all = [...v4, ...v6];
|
|
98
|
+
if (all.length === 0)
|
|
99
|
+
return '';
|
|
100
|
+
return `block drop out quick to { ${all.join(', ')} }\n`;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the hostnames and install firewall rules for their current IPs.
|
|
104
|
+
* Throws on unsupported platforms; individual rule failures are tolerated.
|
|
105
|
+
*/
|
|
106
|
+
export async function applyFirewall(hosts, opts = {}) {
|
|
107
|
+
const runner = opts.runner ?? defaultRunner;
|
|
108
|
+
const platform = opts.platform ?? process.platform;
|
|
109
|
+
const { v4, v6, unresolved } = await resolveTargetIPs(hosts, opts.resolver);
|
|
110
|
+
const blocked = v4.length + v6.length;
|
|
111
|
+
// Individual rule failures are tolerated, but if not a single command
|
|
112
|
+
// succeeded the firewall tool itself is missing/unusable — surface that
|
|
113
|
+
// instead of pretending the IPs are blocked.
|
|
114
|
+
let applied = 0;
|
|
115
|
+
const failIfNothingApplied = () => {
|
|
116
|
+
if (blocked > 0 && applied === 0) {
|
|
117
|
+
throw new Error('no firewall rules could be applied (is the firewall tool available?)');
|
|
118
|
+
}
|
|
119
|
+
return { blocked, unresolved };
|
|
120
|
+
};
|
|
121
|
+
if (platform === 'linux') {
|
|
122
|
+
for (const ip of v4)
|
|
123
|
+
if ((await runner('iptables', linuxAddArgs(ip))).ok)
|
|
124
|
+
applied++;
|
|
125
|
+
for (const ip of v6)
|
|
126
|
+
if ((await runner('ip6tables', linuxAddArgs(ip))).ok)
|
|
127
|
+
applied++;
|
|
128
|
+
return failIfNothingApplied();
|
|
129
|
+
}
|
|
130
|
+
if (platform === 'win32') {
|
|
131
|
+
for (const args of windowsAddArgs([...v4, ...v6])) {
|
|
132
|
+
if ((await runner('netsh', args)).ok)
|
|
133
|
+
applied++;
|
|
134
|
+
}
|
|
135
|
+
return failIfNothingApplied();
|
|
136
|
+
}
|
|
137
|
+
if (platform === 'darwin') {
|
|
138
|
+
const rules = darwinAnchorRules(v4, v6);
|
|
139
|
+
if (rules) {
|
|
140
|
+
await runner('pfctl', ['-e']); // best effort: enable pf if it isn't
|
|
141
|
+
if ((await runner('pfctl', ['-a', RULE_TAG, '-f', '-'], rules)).ok)
|
|
142
|
+
applied++;
|
|
143
|
+
}
|
|
144
|
+
return failIfNothingApplied();
|
|
145
|
+
}
|
|
146
|
+
throw new Error(`--firewall is not supported on platform "${platform}"`);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Remove every killm firewall rule. Needs no stored state, so it also cleans
|
|
150
|
+
* up rules left behind by a crashed run. Failures are tolerated — rules that
|
|
151
|
+
* don't exist can't be removed.
|
|
152
|
+
*/
|
|
153
|
+
export async function removeFirewall(opts = {}) {
|
|
154
|
+
const runner = opts.runner ?? defaultRunner;
|
|
155
|
+
const platform = opts.platform ?? process.platform;
|
|
156
|
+
if (platform === 'linux') {
|
|
157
|
+
for (const tool of ['iptables', 'ip6tables']) {
|
|
158
|
+
const listing = await runner(tool, ['-S', 'OUTPUT']);
|
|
159
|
+
if (!listing.ok)
|
|
160
|
+
continue;
|
|
161
|
+
for (const args of linuxDeleteArgsFromListing(listing.stdout)) {
|
|
162
|
+
await runner(tool, args);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (platform === 'win32') {
|
|
168
|
+
await runner('netsh', windowsDeleteArgs());
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (platform === 'darwin') {
|
|
172
|
+
await runner('pfctl', ['-a', RULE_TAG, '-F', 'rules']);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
}
|
package/dist/src/hosts.js
CHANGED
|
@@ -4,6 +4,8 @@ import path from 'node:path';
|
|
|
4
4
|
import { execFile } from 'node:child_process';
|
|
5
5
|
export const BEGIN = '# >>> killm block (do not edit between markers) >>>';
|
|
6
6
|
export const END = '# <<< killm block <<<';
|
|
7
|
+
/** Machine-parseable expiry marker inside the block. */
|
|
8
|
+
export const EXPIRES_PREFIX = '# killm-expires: ';
|
|
7
9
|
const SINK4 = '0.0.0.0';
|
|
8
10
|
const SINK6 = '::1';
|
|
9
11
|
function errnoCode(err) {
|
|
@@ -59,6 +61,7 @@ export function buildBlock(hosts, opts = {}) {
|
|
|
59
61
|
lines.push(`# added by killm at ${new Date().toISOString()}`);
|
|
60
62
|
if (opts.until) {
|
|
61
63
|
lines.push(`# auto-removed at ${opts.until.toISOString()} (or when killm exits)`);
|
|
64
|
+
lines.push(`${EXPIRES_PREFIX}${opts.until.toISOString()}`);
|
|
62
65
|
}
|
|
63
66
|
for (const h of hosts) {
|
|
64
67
|
lines.push(`${SINK4}\t${h}`);
|
|
@@ -125,6 +128,36 @@ export function removeBlock() {
|
|
|
125
128
|
export function isBlocked() {
|
|
126
129
|
return readHosts().includes(BEGIN);
|
|
127
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* When the current block is scheduled to expire, or null when no block is
|
|
133
|
+
* present or it carries no (valid) expiry marker — e.g. blocks written by
|
|
134
|
+
* killm <= 1.0.2.
|
|
135
|
+
*/
|
|
136
|
+
export function blockExpiry(text = readHosts()) {
|
|
137
|
+
const begin = text.indexOf(BEGIN);
|
|
138
|
+
if (begin === -1)
|
|
139
|
+
return null;
|
|
140
|
+
const idx = text.indexOf(EXPIRES_PREFIX, begin);
|
|
141
|
+
if (idx === -1)
|
|
142
|
+
return null;
|
|
143
|
+
const start = idx + EXPIRES_PREFIX.length;
|
|
144
|
+
const lineEnd = text.indexOf('\n', start);
|
|
145
|
+
const stamp = text.slice(start, lineEnd === -1 ? undefined : lineEnd).trim();
|
|
146
|
+
const date = new Date(stamp);
|
|
147
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Remove the block if its expiry has passed — heals hosts files stranded by
|
|
151
|
+
* a killed process (terminal closed, kill -9, crash, machine shutdown).
|
|
152
|
+
*
|
|
153
|
+
* @returns true when an expired block was found and removed
|
|
154
|
+
*/
|
|
155
|
+
export function clearExpiredBlock(now = new Date()) {
|
|
156
|
+
const expiry = blockExpiry();
|
|
157
|
+
if (expiry === null || expiry.getTime() > now.getTime())
|
|
158
|
+
return false;
|
|
159
|
+
return removeBlock();
|
|
160
|
+
}
|
|
128
161
|
/**
|
|
129
162
|
* Best-effort DNS cache flush so the new hosts entries take effect immediately.
|
|
130
163
|
* Silently ignores failures — the block still works, it may just take a moment.
|
|
@@ -159,3 +192,18 @@ export function hasPrivileges() {
|
|
|
159
192
|
return true;
|
|
160
193
|
return typeof process.getuid === 'function' && process.getuid() === 0;
|
|
161
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Whether we are running inside Windows Subsystem for Linux. A block applied
|
|
197
|
+
* here edits WSL's /etc/hosts only — Windows browsers read the Windows hosts
|
|
198
|
+
* file and are NOT affected.
|
|
199
|
+
*/
|
|
200
|
+
export function isWsl(procVersionPath = '/proc/version') {
|
|
201
|
+
if (process.platform !== 'linux')
|
|
202
|
+
return false;
|
|
203
|
+
try {
|
|
204
|
+
return /microsoft/i.test(fs.readFileSync(procVersionPath, 'utf8'));
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
package/dist/src/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { parseArgs, HELP, VERSION } from './cli.js';
|
|
|
3
3
|
import { resolveTargets } from './targets.js';
|
|
4
4
|
import { formatDuration } from './duration.js';
|
|
5
5
|
import * as hosts from './hosts.js';
|
|
6
|
+
import * as firewall from './firewall.js';
|
|
6
7
|
// Minimal ANSI styling that degrades to plain text when not a TTY.
|
|
7
8
|
const tty = process.stdout.isTTY === true;
|
|
8
9
|
const c = {
|
|
@@ -93,6 +94,15 @@ async function runBlock(parsed) {
|
|
|
93
94
|
return 0;
|
|
94
95
|
}
|
|
95
96
|
}
|
|
97
|
+
// Heal any expired block stranded by a killed process before applying ours.
|
|
98
|
+
try {
|
|
99
|
+
if (hosts.clearExpiredBlock()) {
|
|
100
|
+
out(c.dim(' (cleaned up an expired block left behind by a previous run)'));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
/* no privileges; the apply below will surface the real error */
|
|
105
|
+
}
|
|
96
106
|
const endTime = Date.now() + durationMs;
|
|
97
107
|
const until = new Date(endTime);
|
|
98
108
|
try {
|
|
@@ -110,6 +120,37 @@ async function runBlock(parsed) {
|
|
|
110
120
|
}
|
|
111
121
|
await hosts.flushDns();
|
|
112
122
|
out(c.green(` ✓ block active until ${until.toLocaleTimeString()}`));
|
|
123
|
+
let firewallActive = false;
|
|
124
|
+
if (parsed.firewall) {
|
|
125
|
+
try {
|
|
126
|
+
const result = await firewall.applyFirewall(targets);
|
|
127
|
+
firewallActive = true;
|
|
128
|
+
out(c.green(` ✓ firewall: blocked ${result.blocked} IPs at the OS firewall`));
|
|
129
|
+
if (result.unresolved.length > 0) {
|
|
130
|
+
out(c.dim(` (${result.unresolved.length} hostnames did not resolve and were skipped)`));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
err(c.yellow(` ⚠ firewall rules could not be applied: ${e.message}`));
|
|
135
|
+
err(c.yellow(' Continuing with the hosts-file block only.'));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (hosts.isWsl()) {
|
|
139
|
+
out();
|
|
140
|
+
out(c.yellow(' ⚠ WSL detected: this only blocks processes running inside WSL.'));
|
|
141
|
+
out(c.yellow(' Windows apps (your browser!) read the Windows hosts file and are'));
|
|
142
|
+
out(c.yellow(' NOT affected. To block them, run killm from an Administrator'));
|
|
143
|
+
out(c.yellow(' PowerShell/terminal on Windows: npx killm for 1h --web'));
|
|
144
|
+
}
|
|
145
|
+
out();
|
|
146
|
+
out(c.dim(' note: browsers cache DNS and "Secure DNS" (DoH) bypasses the hosts') +
|
|
147
|
+
'\n' +
|
|
148
|
+
c.dim(' file entirely — restart the browser / disable Secure DNS if needed.'));
|
|
149
|
+
out();
|
|
150
|
+
out(c.yellow(' ⚠ keep this process running — killm lifts the block when the timer'));
|
|
151
|
+
out(c.yellow(' ends or you press Ctrl+C / close the terminal. If it dies harder'));
|
|
152
|
+
out(c.yellow(' than that (kill -9, crash, shutdown), the block stays in place'));
|
|
153
|
+
out(c.yellow(' until you run any killm command again, e.g. killm --restore'));
|
|
113
154
|
out();
|
|
114
155
|
let timer = null;
|
|
115
156
|
let ticker = null;
|
|
@@ -123,6 +164,11 @@ async function runBlock(parsed) {
|
|
|
123
164
|
clearTimeout(timer);
|
|
124
165
|
if (ticker)
|
|
125
166
|
clearInterval(ticker);
|
|
167
|
+
if (firewallActive) {
|
|
168
|
+
// Fire and forget: rules are tagged, so a missed removal here is still
|
|
169
|
+
// recoverable via "killm --restore".
|
|
170
|
+
void firewall.removeFirewall().catch(() => { });
|
|
171
|
+
}
|
|
126
172
|
try {
|
|
127
173
|
const removed = hosts.removeBlock();
|
|
128
174
|
void hosts.flushDns(); // fire and forget on the way out
|
|
@@ -167,6 +213,9 @@ async function runBlock(parsed) {
|
|
|
167
213
|
};
|
|
168
214
|
process.once('SIGINT', () => onSignal('interrupted'));
|
|
169
215
|
process.once('SIGTERM', () => onSignal('terminated'));
|
|
216
|
+
// Closing the terminal window sends SIGHUP; without this handler Node
|
|
217
|
+
// dies without cleanup and the block is stranded in the hosts file.
|
|
218
|
+
process.once('SIGHUP', () => onSignal('terminal closed'));
|
|
170
219
|
});
|
|
171
220
|
return 0;
|
|
172
221
|
}
|
|
@@ -192,9 +241,31 @@ export async function main(argv) {
|
|
|
192
241
|
out(VERSION);
|
|
193
242
|
return 0;
|
|
194
243
|
case 'status': {
|
|
244
|
+
const expiry = hosts.blockExpiry();
|
|
245
|
+
const expired = expiry !== null && expiry.getTime() <= Date.now();
|
|
246
|
+
if (expired) {
|
|
247
|
+
// A previous run was killed before it could clean up. Heal it now.
|
|
248
|
+
try {
|
|
249
|
+
hosts.clearExpiredBlock();
|
|
250
|
+
await hosts.flushDns();
|
|
251
|
+
out(c.green('killm: no block active.'));
|
|
252
|
+
out(c.dim('(cleaned up an expired block left behind by a previous run)'));
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
err(c.yellow('killm: an EXPIRED block is still stuck in the hosts file.'));
|
|
257
|
+
err(' Clean it up with elevated privileges: sudo npx killm --restore');
|
|
258
|
+
return 1;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
195
261
|
if (hosts.isBlocked()) {
|
|
196
262
|
out(c.yellow('killm: a block is currently ACTIVE.'));
|
|
197
|
-
|
|
263
|
+
if (expiry) {
|
|
264
|
+
out(c.dim(`Scheduled to lift at ${expiry.toLocaleString()}.`));
|
|
265
|
+
out(c.dim('If the killm process that created it is no longer running, it will not\n' +
|
|
266
|
+
'lift itself on time — but any later killm command cleans it up once expired.'));
|
|
267
|
+
}
|
|
268
|
+
out(c.dim('Run "killm --restore" to lift it now.'));
|
|
198
269
|
}
|
|
199
270
|
else {
|
|
200
271
|
out(c.green('killm: no block active.'));
|
|
@@ -217,6 +288,13 @@ export async function main(argv) {
|
|
|
217
288
|
try {
|
|
218
289
|
const removed = hosts.removeBlock();
|
|
219
290
|
await hosts.flushDns();
|
|
291
|
+
// Always sweep firewall rules too: they are tagged "killm", so this
|
|
292
|
+
// also cleans up after a crashed --firewall run. Harmless when none
|
|
293
|
+
// exist or the platform is unsupported. Skipped under the test
|
|
294
|
+
// override so tests never touch the real firewall.
|
|
295
|
+
if (!process.env.KILLM_HOSTS_PATH) {
|
|
296
|
+
await firewall.removeFirewall().catch(() => { });
|
|
297
|
+
}
|
|
220
298
|
out(removed
|
|
221
299
|
? c.green('killm: block lifted. Access restored.')
|
|
222
300
|
: c.dim('killm: no block was active.'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "killm",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Temporarily block your machine from reaching LLM services to curb AI dependency. npx killm for 1h --agents",
|
|
5
5
|
"bin": {
|
|
6
6
|
"killm": "dist/src/bin.js"
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"build": "tsc",
|
|
16
16
|
"typecheck": "tsc --noEmit",
|
|
17
17
|
"pretest": "npm run build",
|
|
18
|
-
"test": "node --test dist/test/duration.test.js dist/test/targets.test.js dist/test/cli.test.js dist/test/hosts.test.js dist/test/e2e.test.js",
|
|
18
|
+
"test": "node --test dist/test/duration.test.js dist/test/targets.test.js dist/test/cli.test.js dist/test/hosts.test.js dist/test/firewall.test.js dist/test/e2e.test.js",
|
|
19
19
|
"lint": "eslint .",
|
|
20
20
|
"lint:fix": "eslint . --fix",
|
|
21
21
|
"format": "prettier --write .",
|