@ai-ecoverse/slicc-kernel 1.130.1 → 1.131.0
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 +2 -2
- package/dist/kernel-worker.js +138 -10
- package/dist/kernel-worker.js.map +4 -4
- package/dist/node-process-worker.js +1 -1
- package/dist/node.js +138 -10
- package/dist/node.js.map +4 -4
- package/dist/process-worker.js +1 -1
- package/dist/types/kernel/net/sealed-pg.d.ts +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -487,13 +487,13 @@ The module's default export (or its `main` export) is called with a context and
|
|
|
487
487
|
- `AUTH PLAIN <blob>`, or the line after a bare `AUTH PLAIN`: the kernel decodes the blob, and asks the vault (`auth-plain`) only when the placeholders are in the **password field**. A placeholder in the user or authorization identity goes out as written. It substitutes there and re-encodes.
|
|
488
488
|
- `AUTH LOGIN`: the password line (the second line, or the first after `AUTH LOGIN <user>`) is decoded, substituted (`auth-login`) and re-encoded; the user line goes out as written.
|
|
489
489
|
- Every other line goes out as written. After the first `AUTH`, or once `MAIL`, `RCPT`, `DATA` or `BDAT` is seen, so is the rest of the session. A line still unfinished after 2 s, or longer than 4 KiB, is sent as written, and so is the rest.
|
|
490
|
+
- **pg:** a session that starts with a protocol 3 `StartupMessage` (after `sslmode`'s `SSLRequest` upgrade, or with direct TLS) is followed message by message. The only position is the **`PasswordMessage` answering `AuthenticationCleartextPassword` (`R` 3)**, as the program read it from the server. The password is substituted (`password-cleartext`) and the message is framed again. Anything else goes out as written: the startup, an MD5 (`R` 5) or SCRAM (`R` 10) answer, every later message, and a frame over 16 KiB. MD5 and SCRAM need the kernel to compute the answer from the password, so they aren't supported yet. A server whose `pg_hba` asks for them gets the placeholder, and refuses.
|
|
490
491
|
- **What goes out:** the placeholders the vault answers with a value of at least 8 bytes go out as that value (`Basic` decoded, substituted and re-encoded). Everything else goes out as written:
|
|
491
492
|
- the request line and the body;
|
|
492
493
|
- other headers;
|
|
493
494
|
- a head with placeholders in two header lines;
|
|
494
495
|
- later requests on the same connection;
|
|
495
496
|
- anything once the head passes 16 KiB, or once the program pauses for 2 s in the middle of it;
|
|
496
|
-
- pg, which comes next.
|
|
497
497
|
- **What's read back:** once a process has had a value substituted for a host, everything its whole process tree reads from any A2 session to that host is scrubbed: the process itself, its ancestors and every descendant (forked, spawned or exec'd, before or after the substitution; a forked child's own new session included) with [slicc-scrub](https://github.com/ai-ecoverse/slicc-scrub), as S2 does: the value comes back as its placeholder, and a fragment or an encoded form (JSON, percent, HTML, base64, hex) as `[redacted:sealed]`. The scrub stays until the whole tree has ended. A process outside the tree isn't covered. A value shorter than 8 bytes is never substituted, as it couldn't be scrubbed.
|
|
498
498
|
- **The bound:** the server the value goes to is the one the vault rule names, verified against the kernel's own roots. Substitution trusts that server not to reflect the value back in a form the scrub doesn't know (encrypted under a key the program chose, say). That's the same residual as A1's.
|
|
499
499
|
- **Elsewhere:** a placeholder written into any other A2 session reaches the server as written. It isn't the secret, so nothing leaks.
|
|
@@ -986,7 +986,7 @@ The kernel then asks the hook `{ v: 1, op: 'materializeConsume', ticket, stamp,
|
|
|
986
986
|
**The child's output is scrubbed (S2).** The kernel compiles a [slicc-scrub](https://github.com/ai-ecoverse/slicc-scrub) scrubber (1.0.0, bundled; the published package has no runtime dependency) over exactly the values it swapped in, and gives the child new open file descriptions for fd 1 and fd 2, for every other descriptor it holds on the same descriptions (a `dup` of them), and for every descriptor on a terminal (fd 0 of a terminal session is one), that pass every write through it into the originals (one wrapper per original description, so `2>&1` keeps its order). A value appears as its placeholder, and a fragment or encoded form of it (JSON, percent, HTML, base64, hex, wrapped) as `[redacted:NAME]`. Bytes go through at once unless they may still be the start of a match; such a tail is let out after 50 ms without a write when it's shorter than 16 bytes, and otherwise at the next write or at close. The child's descendants inherit the scrubbed descriptions through fork, spawn, exec and `dup`, and a descendant that opens its controlling terminal (`/dev/tty`, or a terminal by name) gets one scrubbed too, as does a pseudo-terminal slave (`/dev/pts/N`) or [`/dev/clipboard`](#devclipboard) it opens, and a `/dev/clipboard` descriptor it inherited is wrapped at the redeem like fd 1 and 2, so the page's clipboard gets placeholders, never the values. Files, sockets and pipes the child or a descendant opens itself are not scrubbed, so `printf %s "$T" > f` writes the value. A scrubbed fd 1 or 2 is a stream: it doesn't seek. Under `O_NONBLOCK` it gives `EAGAIN` when the pipe or terminal under it would. A write that finds room returns at once, taking at most 64 KiB, and the scrubbed bytes the file couldn't take yet go out before anything written later. Until they have, `poll` reports it unwritable and a nonblocking write gets `EAGAIN`, taking nothing. A blocking write that waits for them, or for room, ends with `EINTR` at a signal, taking nothing; one interrupted after it began writing returns its whole count, and the rest follows. A descendant that redeems a ticket of its own adds its values: its output is scrubbed for both sets. A value under 8 bytes is swapped but can't be scrubbed. The scrubber goes when the last process and description holding it do.
|
|
987
987
|
|
|
988
988
|
**A2 sessions (`sessionMaterialize`, [#1090](https://github.com/ai-ecoverse/slicc-kernel/issues/1090)).** For a kernel-terminated TLS session (see **Sealed values in an A2 session** under JS programs' `ctx.net`), the kernel asks the hook `{ v: 1, op: 'sessionMaterialize', caller, stamp, session, host, port, protocol, position, verified, handshakeAt, exit, placeholders, head? }` (slicc-vault 1.10.0, Sessions), once per session:
|
|
989
|
-
- `stamp` (also `caller`) is the stamp of the process that owns the socket; `session` the kernel's id for it (16 to 128 of `A-Za-z0-9_-`); `host` the name the leaf was verified against, lower-cased without a trailing dot; `handshakeAt` when the handshake finished (epoch ms); `exit` the exit the session leaves by, as the uplink reported it (`tailscale` or `device:<id>`, the id of `[A-Za-z0-9._
|
|
989
|
+
- `stamp` (also `caller`) is the stamp of the process that owns the socket; `session` the kernel's id for it (16 to 128 of `A-Za-z0-9_-`); `host` the name the leaf was verified against, lower-cased without a trailing dot; `handshakeAt` when the handshake finished (epoch ms); `exit` the exit the session leaves by, as the uplink reported it (`tailscale` or `device:<id>`, the id of `[A-Za-z0-9._:-]`);
|
|
990
990
|
- `protocol` is `http1`, `smtp` or `pg`, and `position` one of its own: `header:<lower-cased name>`, `auth-plain` or `auth-login`, or `password-cleartext`; `head` (`{ method, path }`) is there for `http1` only, and required there;
|
|
991
991
|
- `placeholders` are the 1 to 16 distinct `fnox_proxy_<24 hex>` placeholders found at that position.
|
|
992
992
|
|
package/dist/kernel-worker.js
CHANGED
|
@@ -7022,7 +7022,7 @@ function satisfies(version, range2) {
|
|
|
7022
7022
|
}
|
|
7023
7023
|
|
|
7024
7024
|
// src/version.ts
|
|
7025
|
-
var KERNEL_VERSION = true ? "1.
|
|
7025
|
+
var KERNEL_VERSION = true ? "1.131.0" : "0.0.0-dev";
|
|
7026
7026
|
var OS_RELEASE = "6.0.0-slicc";
|
|
7027
7027
|
function procVersion() {
|
|
7028
7028
|
return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
|
|
@@ -17746,7 +17746,7 @@ var TOKEN5 = "[!#$%&'*+.^_`|~0-9a-z-]+";
|
|
|
17746
17746
|
var HEADER = new RegExp(`^header:${TOKEN5}$`);
|
|
17747
17747
|
var METHOD = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
17748
17748
|
var TARGET = /^[\x21-\x7e]+$/;
|
|
17749
|
-
var SESSION_EXIT = /^(tailscale|device:[A-Za-z0-9._
|
|
17749
|
+
var SESSION_EXIT = /^(tailscale|device:[A-Za-z0-9._:-]+)$/;
|
|
17750
17750
|
var HOST = /^[a-z0-9.:-]+$/;
|
|
17751
17751
|
var UNSAFE = /[\r\n\0]/;
|
|
17752
17752
|
var POSITIONS = {
|
|
@@ -18495,6 +18495,90 @@ async function substituteHead(head, ask) {
|
|
|
18495
18495
|
`, values };
|
|
18496
18496
|
}
|
|
18497
18497
|
|
|
18498
|
+
// src/kernel/net/sealed-pg.ts
|
|
18499
|
+
var PG_MESSAGE_MAX = 16 * 1024;
|
|
18500
|
+
var PROTOCOL_3 = 196608;
|
|
18501
|
+
var CLEARTEXT = 3;
|
|
18502
|
+
function int32(bytes, at) {
|
|
18503
|
+
return bytes[at] << 24 | bytes[at + 1] << 16 | bytes[at + 2] << 8 | bytes[at + 3];
|
|
18504
|
+
}
|
|
18505
|
+
function sniffPg(start2) {
|
|
18506
|
+
const bytes = Uint8Array.from(start2, (c) => c.charCodeAt(0));
|
|
18507
|
+
if (bytes.length === 0 || bytes[0] !== 0) return bytes.length === 0 ? "more" : "no";
|
|
18508
|
+
if (bytes.length < 8) return "more";
|
|
18509
|
+
const length2 = int32(bytes, 0);
|
|
18510
|
+
return length2 >= 8 && length2 <= PG_MESSAGE_MAX && int32(bytes, 4) === PROTOCOL_3 ? "yes" : "no";
|
|
18511
|
+
}
|
|
18512
|
+
var PgServer = class {
|
|
18513
|
+
buffer = new Uint8Array(0);
|
|
18514
|
+
skip = 0;
|
|
18515
|
+
lost = false;
|
|
18516
|
+
cleartext = false;
|
|
18517
|
+
feed(bytes) {
|
|
18518
|
+
if (this.lost) return;
|
|
18519
|
+
const data = new Uint8Array(this.buffer.length + bytes.length);
|
|
18520
|
+
data.set(this.buffer);
|
|
18521
|
+
data.set(bytes, this.buffer.length);
|
|
18522
|
+
let at = 0;
|
|
18523
|
+
for (; ; ) {
|
|
18524
|
+
const left = data.length - at;
|
|
18525
|
+
if (this.skip > 0) {
|
|
18526
|
+
const n = Math.min(this.skip, left);
|
|
18527
|
+
this.skip -= n;
|
|
18528
|
+
at += n;
|
|
18529
|
+
if (this.skip > 0) break;
|
|
18530
|
+
continue;
|
|
18531
|
+
}
|
|
18532
|
+
if (left < 5) break;
|
|
18533
|
+
const type = data[at];
|
|
18534
|
+
const whole = 1 + int32(data, at + 1);
|
|
18535
|
+
if (whole < 5 || type === 82 && whole > PG_MESSAGE_MAX) {
|
|
18536
|
+
this.lost = true;
|
|
18537
|
+
this.cleartext = false;
|
|
18538
|
+
this.buffer = new Uint8Array(0);
|
|
18539
|
+
return;
|
|
18540
|
+
}
|
|
18541
|
+
if (type !== 82) {
|
|
18542
|
+
this.skip = whole;
|
|
18543
|
+
continue;
|
|
18544
|
+
}
|
|
18545
|
+
if (left < whole) break;
|
|
18546
|
+
this.cleartext = whole === 9 && int32(data, at + 5) === CLEARTEXT;
|
|
18547
|
+
at += whole;
|
|
18548
|
+
}
|
|
18549
|
+
this.buffer = data.slice(at);
|
|
18550
|
+
}
|
|
18551
|
+
};
|
|
18552
|
+
function frame3(type, body) {
|
|
18553
|
+
const out = new Uint8Array(5 + body.length);
|
|
18554
|
+
out[0] = type;
|
|
18555
|
+
const length2 = 4 + body.length;
|
|
18556
|
+
out.set([length2 >>> 24 & 255, length2 >>> 16 & 255, length2 >>> 8 & 255, length2 & 255], 1);
|
|
18557
|
+
for (let i = 0; i < body.length; i++) out[5 + i] = body.charCodeAt(i);
|
|
18558
|
+
return out;
|
|
18559
|
+
}
|
|
18560
|
+
function pgLength(bytes, started) {
|
|
18561
|
+
const header3 = started ? 5 : 4;
|
|
18562
|
+
if (bytes.length < header3) return void 0;
|
|
18563
|
+
const length2 = int32(bytes, started ? 1 : 0);
|
|
18564
|
+
return started ? 1 + length2 : length2;
|
|
18565
|
+
}
|
|
18566
|
+
async function pgPassword(message3, server, ask) {
|
|
18567
|
+
if (message3[0] !== 112 || !server.cleartext) return { bytes: message3, done: true };
|
|
18568
|
+
server.cleartext = false;
|
|
18569
|
+
let body = "";
|
|
18570
|
+
for (const b of message3.subarray(5)) body += String.fromCharCode(b);
|
|
18571
|
+
if (!body.endsWith("\0") || body.indexOf("\0") !== body.length - 1)
|
|
18572
|
+
return { bytes: message3, done: true };
|
|
18573
|
+
const password = body.slice(0, -1);
|
|
18574
|
+
const placeholders = placeholdersIn(password);
|
|
18575
|
+
if (placeholders.length === 0 || placeholders.length > SESSION_PLACEHOLDERS)
|
|
18576
|
+
return { bytes: message3, done: true };
|
|
18577
|
+
const values = usable(await ask(placeholders));
|
|
18578
|
+
if (Object.keys(values).length === 0) return { bytes: message3, done: true };
|
|
18579
|
+
return { bytes: frame3(112, `${replaced(password, values)}\0`), values, done: true };
|
|
18580
|
+
}
|
|
18581
|
+
|
|
18498
18582
|
// src/kernel/net/sealed-smtp.ts
|
|
18499
18583
|
var SMTP_LINE_MAX = 4096;
|
|
18500
18584
|
var GREETING = ["EHLO ", "HELO "];
|
|
@@ -18616,11 +18700,14 @@ function headEnd2(bytes) {
|
|
|
18616
18700
|
return -1;
|
|
18617
18701
|
}
|
|
18618
18702
|
function modeOf(start2) {
|
|
18619
|
-
const
|
|
18620
|
-
|
|
18621
|
-
|
|
18622
|
-
|
|
18623
|
-
|
|
18703
|
+
const sniffs = [
|
|
18704
|
+
["http1", sniffHttp1(start2)],
|
|
18705
|
+
["smtp", sniffSmtp(start2)],
|
|
18706
|
+
["pg", sniffPg(start2)]
|
|
18707
|
+
];
|
|
18708
|
+
const hit = sniffs.find(([, v]) => v === "yes");
|
|
18709
|
+
if (hit) return hit[0];
|
|
18710
|
+
return sniffs.every(([, v]) => v === "no") ? "pass" : void 0;
|
|
18624
18711
|
}
|
|
18625
18712
|
function crlf(bytes) {
|
|
18626
18713
|
for (let i = 0; i + 1 < bytes.length; i++) if (bytes[i] === 13 && bytes[i + 1] === 10) return i;
|
|
@@ -18634,12 +18721,15 @@ var SessionWriter = class {
|
|
|
18634
18721
|
failed;
|
|
18635
18722
|
session = sessionId();
|
|
18636
18723
|
smtp;
|
|
18724
|
+
pgStarted = false;
|
|
18637
18725
|
conn;
|
|
18638
18726
|
s;
|
|
18727
|
+
pg;
|
|
18639
18728
|
scope;
|
|
18640
|
-
constructor(conn, s, scope) {
|
|
18729
|
+
constructor(conn, s, pg, scope) {
|
|
18641
18730
|
this.conn = conn;
|
|
18642
18731
|
this.s = s;
|
|
18732
|
+
this.pg = pg;
|
|
18643
18733
|
this.scope = scope;
|
|
18644
18734
|
this.smtp = new SmtpClient(
|
|
18645
18735
|
(position, placeholders) => this.ask("smtp", position, placeholders)
|
|
@@ -18688,6 +18778,7 @@ var SessionWriter = class {
|
|
|
18688
18778
|
if (this.mode === "pass") await this.release();
|
|
18689
18779
|
else if (this.mode === "http1") await this.http1();
|
|
18690
18780
|
else if (this.mode === "smtp") await this.lines();
|
|
18781
|
+
else if (this.mode === "pg") await this.messages();
|
|
18691
18782
|
if (this.mode !== "pass" && this.buffer.length > 0)
|
|
18692
18783
|
this.timer = setTimeout(() => this.idle(), HEAD_IDLE_MS);
|
|
18693
18784
|
return bytes.length;
|
|
@@ -18718,6 +18809,33 @@ var SessionWriter = class {
|
|
|
18718
18809
|
}
|
|
18719
18810
|
if (this.buffer.length > SMTP_LINE_MAX) await this.release();
|
|
18720
18811
|
}
|
|
18812
|
+
async messages() {
|
|
18813
|
+
for (; ; ) {
|
|
18814
|
+
const whole = pgLength(this.buffer, this.pgStarted);
|
|
18815
|
+
if (whole === void 0 || whole < 4 || whole > PG_MESSAGE_MAX) {
|
|
18816
|
+
if (whole !== void 0) await this.release();
|
|
18817
|
+
return;
|
|
18818
|
+
}
|
|
18819
|
+
if (this.buffer.length < whole) return;
|
|
18820
|
+
const message3 = this.buffer.slice(0, whole);
|
|
18821
|
+
this.buffer = this.buffer.slice(whole);
|
|
18822
|
+
const started = this.pgStarted;
|
|
18823
|
+
this.pgStarted = true;
|
|
18824
|
+
const out = started ? await pgPassword(
|
|
18825
|
+
message3,
|
|
18826
|
+
this.pg,
|
|
18827
|
+
(placeholders) => this.ask("pg", "password-cleartext", placeholders)
|
|
18828
|
+
).catch(() => ({ bytes: message3, done: true })) : { bytes: message3, done: false };
|
|
18829
|
+
if ("values" in out && out.values) this.remember(out.values);
|
|
18830
|
+
try {
|
|
18831
|
+
await writeAll4(this.conn, out.bytes);
|
|
18832
|
+
} finally {
|
|
18833
|
+
out.bytes.fill(0);
|
|
18834
|
+
message3.fill(0);
|
|
18835
|
+
}
|
|
18836
|
+
if (out.done) return this.release();
|
|
18837
|
+
}
|
|
18838
|
+
}
|
|
18721
18839
|
idle() {
|
|
18722
18840
|
this.chain = this.chain.then(() => this.release()).catch((err) => {
|
|
18723
18841
|
this.failed ??= err;
|
|
@@ -18831,9 +18949,18 @@ function idle() {
|
|
|
18831
18949
|
return new Promise((resolve2) => setTimeout(() => resolve2(IDLE2), SCRUB_IDLE_MS));
|
|
18832
18950
|
}
|
|
18833
18951
|
function sealedConn(conn, s) {
|
|
18952
|
+
const pg = new PgServer();
|
|
18834
18953
|
const scope = s.broker.sessions.scope(s.pid, s.host);
|
|
18835
|
-
const writer = substitutes(s) ? new SessionWriter(conn, s, scope) : void 0;
|
|
18836
|
-
const
|
|
18954
|
+
const writer = substitutes(s) ? new SessionWriter(conn, s, pg, scope) : void 0;
|
|
18955
|
+
const observed = {
|
|
18956
|
+
...conn,
|
|
18957
|
+
read: async () => {
|
|
18958
|
+
const bytes = await conn.read();
|
|
18959
|
+
if (bytes) pg.feed(bytes);
|
|
18960
|
+
return bytes;
|
|
18961
|
+
}
|
|
18962
|
+
};
|
|
18963
|
+
const reader = new ScrubReader(writer ? observed : conn, scope);
|
|
18837
18964
|
return {
|
|
18838
18965
|
localAddr: conn.localAddr,
|
|
18839
18966
|
remoteAddr: conn.remoteAddr,
|
|
@@ -24091,6 +24218,7 @@ var SETUP_OPS = /* @__PURE__ */ new Set([
|
|
|
24091
24218
|
"proc-identity",
|
|
24092
24219
|
"fd-info",
|
|
24093
24220
|
"sig-mask",
|
|
24221
|
+
"sig-inherited",
|
|
24094
24222
|
"proc-setpgid",
|
|
24095
24223
|
"tty-pgrp-set",
|
|
24096
24224
|
"proc-exec",
|