OpenVessel

カーネル

このページの見出し
  1. 受け持つこと
  2. スレッドの構成
  3. システムコール
  4. エラーと errno の値
  5. 仮想ネットワーク
  6. 外への接続
  7. ES モジュールの配信
  8. 組み込みのプログラム
  9. 起動時のファイルシステム
  10. 既定の環境変数

カーネルは OpenVessel の「OS」です。Web Worker 1 つが、ファイルシステム・プロセスの表・パイプ・端末・ソケット・仮想ポート・ネットワークへの出口を受け持ちます。このページでは、その部品、スレッドの構成、システムコールの全一覧、仮想ネットワーク、外への HTTP の橋渡し、ES モジュールの配信、組み込みのプログラム、起動時にあるものを説明します。

ファイルシステムとプロセスには、それぞれ専用のページがあります(ファイルシステム・プロセス)。全体の流れは OpenVessel のしくみ を見てください。

受け持つこと

分野カーネルがすることソース
ファイルシステムメモリ上の VFS。inode・権限・時刻・シンボリックリンク・ハードリンク・FIFO・ソケット・変更の通知src/kernel/vfs.ts
プロセスpid・プロセスグループ・argv/env/cwd・ファイル記述子・終了状態・シグナル・execvesrc/kernel/kernel.ts、src/kernel/proc.ts
入出力オープンファイル記述・パイプ・socketpair・TTY の行の規律src/kernel/io.ts
システムコールnode のスレッドからの同期の呼び出しと、非同期のメッセージsrc/kernel/syscalls.ts
仮想ネットワークTCP のポート・Unix ドメインソケット・UDP のポート・プレビューからの HTTP/WebSocketsrc/kernel/net.ts、src/kernel/tcp.ts
外への出口生のソケットで外のホストへ送る HTTP/1.1 を、ネットワークの方針に通して送るsrc/kernel/outbound.ts
ES モジュールVFS のファイルを、ブラウザの ES モジュールとしてプロセスの Worker に配るsrc/kernel/esm.ts
プログラムsh・coreutils・npm/pnpm/yarn/bun・curl/wget。カーネルの中で動くsrc/kernel/sh/、src/kernel/coreutils.ts、src/kernel/npm/、src/kernel/curl.ts
ホストとの通信路埋め込む側のページが使う API(ホスト API・通信の形式)src/kernel/index.ts
ネットワークの方針要求を外へ出してよいかを決める(ネットワークの方針)src/kernel/index.ts、src/shared/policy.ts

src/kernel/base.ts には定数が 1 つだけあります。RUNTIME_BASE は、カーネルのスクリプトを読み込んだディレクトリの URL(末尾の / なし)です。ランタイムの Service Worker は、この下の /__ov/… に答えます。

スレッドの構成

Worker

Worker作るもの名前動かすもの
カーネルランタイムの iframe(src/runtime/runtime.ts)openvessel-kerneldist/runtime/kernel.js。VFS・プロセスの表・すべての組み込みプログラム(sh・coreutils・npm など)
プロセスのメインスレッドカーネルnode:<pid>:<tid>(待機列から取ったものは node (idle))node のプロセス 1 つ分の Node.js ランタイム
ワーカースレッドプロセスの求めに応じてカーネルが作る(worker_threads)メインスレッドと同じ同じプロセスの中のもう 1 つの Node.js ランタイム(pid も fd の表も同じ)

組み込みのプログラムは Worker ではありません。カーネル自身のイベントループの上で動く async 関数です(queueMicrotask で始まります)。入力や子プロセスを待つあいだは処理をカーネルに返すので、カーネルはそのあいだもほかのプロセスに応え続けます。

温めた Worker の待機列

プロセスの起動にかかる時間の大半は、ランタイムのスクリプトの読み込みとコンパイルです。そこでカーネルは、何もしていないプロセス用の Worker を用意しておきます(src/kernel/index.ts)。

  • ランタイムのスクリプトの URL ごとに WARM = 2 個です。
  • ある URL の待機列は、その URL を使う最初のプロセスの起動のときに作られます。Worker を 1 つ取るたびに(setTimeout(…, 0) で)補充します。
  • 待機中の Worker は、init のメッセージを受け取るまで何もしません。待機列が空なら、その場で新しく作ります。
  • ワーカースレッドも、メインスレッドと同じ待機列から取ります。

Node.js のランタイムの選択

ビルドには Node.js のランタイムが 2 つ入っています。

ランタイムスクリプトソース使われるとき
旧ランタイムdist/runtime/process.jssrc/node/(手書きの Node 互換の層)既定
移植ランタイムdist/runtime/node-process.jssrc/nodejs/(Node 自身の lib/ と、TypeScript で書いた internalBinding())VFS のファイル /etc/openvessel/node の中身が port のとき

カーネルは Worker を作るたびに(プロセスの起動ごと、ワーカースレッドごとに)/etc/openvessel/node を読み、前後の空白を除いて比べます。ほかの中身のときや、ファイルが無いときは旧ランタイムです。公開している互換性の数字(互換性)は、移植ランタイムのものです。

// これから起動する node のプロセスをすべて移植ランタイムにする
await vessel.mount({ node: 'port' }, { path: '/etc/openvessel' });
mkdir -p /etc/openvessel && echo port > /etc/openvessel/node

同期のシステムコール

node のスレッドは、SharedArrayBuffer の通信路を通して、カーネルを同期で呼びます(src/shared/syncchan.ts)。

  1. スレッドが要求(UTF-8 の JSON と、必要ならその後ろにバイナリ)をバッファに書き、状態の値を 1 にして、カーネルに { t: 'sys' } を送ります。
  2. 状態の値が変わるまで Atomics.wait で眠ります。
  3. カーネルは要求を読んでシステムコールを実行し、応答を書き、状態の値を 2 にして Atomics.notify を呼びます。
定数値
スレッドごとの通信路の大きさ4 MiB(DEFAULT_CHANNEL_BYTES)
ヘッダー64 バイト。状態・JSON の長さ・バイナリの長さ・この回のバイト数・フラグ(1 = 続きがある)
もっと大きいメッセージどちら向きも、何回かに分けて送る
スレッドごとに同時に出せる呼び出し1 つ

要求は { op, ...args } と、必要ならバイト列です。応答は { ok: 値 }、{ ok: 値, bin: true } とバイト列、または { err: コード, msg: メッセージ } です。システムコールは非同期に終わることもあります(パイプからの読み取り、 spawnSync)。そのあいだ呼んだスレッドは止まったままで、カーネルはほかのすべてに応え続けます。

非同期のメッセージ

システムコールのほかに、スレッドとカーネルは postMessage のメッセージをやり取りします。

カーネル → プロセスのスレッド:

メッセージ項目意味
initkind(main/thread)・pid・ppid・tid・argv・env・cwd・sab・ttys・hostname・スレッドの項目ランタイムを始める(プロセス)
signalsigプロセスが受け手を登録したシグナル、または SIGWINCH
child-exitpid・code・signalspawn で起動した子が終わった(起動したスレッドへ送る)
fd-datafd または id・data(null = EOF)・handle?・err?readStart/readOnce のデータ
fd-writablefdパイプ・ソケットのバッファが空いた(writeWait への返事)
watchid・type・path監視しているパスの下での変更
pingid「前に送ったものは全部処理したか」(pong で答える)
tcp-acceptport・fd・remoteAddress・remotePort待ち受けているポートへの新しい TCP の接続
pipe-acceptport・fd・remoteAddress・remotePortUnix ドメインソケットへの新しい接続
tls-peerfd・certs相手の TLS の証明書(プロセスどうしの TLS)
udp-msgport・data・fromデータグラム
thread-exittid・code・initFailed?・terminated?ワーカースレッドが終わった(親のスレッドへ送る)
thread-gonetid見張っていたスレッドがもう無い
http-req・http-body・http-body-end・http-abort・ws-msg・ws-closerid など旧ランタイムの HTTP サーバーへの要求
loop-res・loop-res-data・loop-res-end・loop-res-errorcrid など旧ランタイムのループバックの要求への応答

プロセスのスレッド → カーネル:

メッセージ意味
sys通信路でシステムコールが待っている
pongping への返事
readStart / readStopfd の fd-data の送り出しを始める・止める。パイプとソケットはデータがあるときだけ読むので、readStop でデータがなくなることはない
readOnce1 回だけ読み(len まで。既定 65536)、id 付きの fd-data で答える
writeWaitこのパイプ・ソケットのバッファが空になったら(または読む人がいなくなったら)fd-writable を送る
threadSpawn / threadKillワーカースレッドを始める・止める
loop-req・loop-body・loop-body-end・loop-abort旧ランタイム: 仮想ポートへの HTTP の要求
http-res・http-res-data・http-res-end・http-res-error・ws-open・ws-send・ws-close旧ランタイム: サーバーの応答

システムコール

すべて src/kernel/syscalls.ts の kernel.syscall が振り分けます。パスはプロセスの作業ディレクトリから解決し、字面の上で正規化します(.. は探す前に取り除きます)。知らない名前は ENOSYS(unknown syscall <op>)です。最後の列は、どちらのランタイムが使うかです(P = 移植、L = 旧)。

ファイル

システムコール引数結果エラー補足使う側
statp・l(lstat)stat のオブジェクトENOENT・ENOTDIR・ELOOP・ENAMETOOLONG呼んだプロセスの /proc/self・/proc/<pid>・/proc/<pid>/fd・/proc/self/fd/N にも答える(特別なファイル)P, L
fstatfdstat のオブジェクトEBADFinode の無い fd は作った値(tty S_IFCHR|0620、パイプ S_IFIFO|0600、ソケット S_IFSOCK|0600)P, L
fdKindfdfile・dir・pipe・tty・null・socket または null—P
accessp・modenull探すときのエラー・EACCES調べるのは X_OK(ビット 1)だけ。ディレクトリ以外で x のビットが 1 つも無いときP, L
openp・flags(0)・mode(0o666)fd(空いている 3 以上の最小の番号)ENOENT・ENOTDIR・EEXIST・EISDIR・EACCES・ENXIO・ELOOP・ENAMETOOLONGO_CREAT で、書き込みと検索の権限の無いディレクトリに作ろうとすると EACCESP, L
closefdnullEBADFその fd でカーネルが始めた読み取りも止めるP, L
readfd・len(65536、最大 16 MiB)・posバイト列EBADF・EISDIRパイプ・tty・ソケットは、データか EOF が来るまで待つP, L
writefd・pos・nonblock・sendHandle とバイト列書いたバイト数EBADF・EISDIR・EPIPE・EINVALパイプ・ソケットに nonblock で書くと、入る分だけ書く(0 = いっぱい)。sendHandle はハンドルを最初の 1 バイトに添える(ソケットだけ)P, L
lseekfd・offset・whence(0/1/2)新しい位置ESPIPE・EINVAL・EBADF通常のファイルだけ。末尾より先へも動けるL(WASI)
readFilepバイト列ENOENT・EISDIR などキャラクタデバイスは 1 回分(最大 64 KiB)だけ読む。/proc/self/fd/N はその fd を EOF まで読むP, L
writeFilep・flags(0o1101)・mode(0o666)とバイト列nullEEXIST・EISDIR・ENOENT・ENOTDIR・EACCESO_APPEND は open を通す。mode は umask で削るP, L
readdirp・types名前、または [名前, S_IF* の種類] の組ENOENT・ENOTDIR作った順。./.. は無いP, L
mkdirp・mode(0o777)・recursiverecursive なら最初に作ったディレクトリか nullEEXIST・ENOENT・ENOTDIR・EACCESP, L
mkdtempprefix作ったパスEACCES・ENOENT・EEXISTランダムな 6 文字 [A-Za-z0-9]、mode 0o700、最大 100 回試すP, L
rmdirpnullENOENT・ENOTDIR・ENOTEMPTY・EACCESP, L
rmp・recursive・forcenullENOENT・ERR_FS_EISDIR権限は調べないL
unlinkpnullENOENT・EISDIR・EACCESP, L
renamefrom・tonullENOENT・EINVAL・EISDIR・ENOTDIR・ENOTEMPTY・EACCESP, L
linkfrom・tonullEPERM・EEXIST・ENOENT・EACCESハードリンクP, L
symlinktarget・pnullEEXIST・ENOENT・EACCESリンク先は渡されたまま記録するP, L
readlinkp文字列EINVAL・ENOENT/proc/self → pid。/proc/self/fd/N → パス・/dev/tty・pipe:[id]・socket:[id]P, L
realpathp文字列ENOENT・ENOTDIR・ELOOPP, L
chmodp・modenull探すときのエラーシンボリックリンクはたどるP, L
fchmodfd・modenullEBADFinode の無い fd には何もしないP, L
utimesp・atime・mtime(ミリ秒)・lnull探すときのエラーシンボリックリンクはいつもたどる(l は無視)P, L
futimesfd・atime・mtime(ミリ秒)nullEBADFP, L
truncatep・len(0)nullEISDIR・探すときのエラー通常のファイル以外はそのままL
ftruncatefd・len(0)nullEBADF・EINVALP, L
copyFilefrom・to・exclnullEISDIR・EEXIST・ENOENTmode のビットも写すP, L
cpfrom・to・force・errorOnExist・dereferencenullEEXIST・EISDIR再帰。コピー先の親ディレクトリも作るL
watchid・p・recursivenull—通知は watch のメッセージで送る(監視)P, L
unwatchidnull—P, L

モジュール

システムコール引数結果使う側
resolvekind(esm/cjs)・spec・from・conditions・exts・paths解決したパス、または解決の記録L
moduleFormatpmodule・commonjs・json・wasm・addon・module-typescript・commonjs-typescript または nullL
esmRegisterid・sourcenull。プロセスが終わるまで、そのソースを /__ov/mw/<pid>/<id>.mjs で配るP

解決の条件は、ESM が import・node・module-sync・default、CommonJS が require・node・module-sync・default で、渡された conditions(--conditions)を足します。

プロセスとスレッド

システムコール引数結果エラー補足使う側
getcwd—作業ディレクトリ—P
chdirp実際のパス(シンボリックリンクを解決したもの)ENOENT・ENOTDIRP, L
umaskmask(省略可)前の値—mask & 0o777 を記録するP
exitcodenull—終了状態は code & 0xff。終了 を参照P, L
execveargv・env・argv0うまくいけば返事は無いENOENT・EACCES・ELOOP何かを変える前に調べるP
killpid・signullEINVAL・ESRCHpid 0 = 呼んだプロセス自身、負 = プロセスグループ、sig 0 = あるかどうかを調べるだけP, L
sigactionsig・onnull—プロセスがそのシグナルを自分で受けると記録するP, L
spawnargv・argv0・env・cwd・stdio・keepStdioOnError{ pid, fds, error? }ENOENT・EACCES・ELOOP・EINVALfds は、パイプの親の側の端P, L
spawnSyncargv・argv0・env・cwd・stdio・timeout・killSignal と標準入力のバイト列{ pid, status, signal, outLen, error? } と標準出力・標準エラーのバイト列spawn と同じ時間切れのあとは error: 'ETIMEDOUT'P, L
threadReservenodeIdtid、または { tid, threadId }—tid は 100000 から。threadId はプロセスごとに 1, 2, …P, L
watchThreadtidnull—そのスレッドが終わったら thread-gone(もう無ければすぐ)P
threadExitcodenull—呼んだワーカースレッドを終える。親は thread-exit を受け取るP, L

端末

システムコール引数結果エラー使う側
isattyfdfd が TTY なら true—L
ttySizefd{ cols, rows } または null—P, L
setRawModefd・rawnullENOTTYP, L

ネットワーク

システムコール引数結果エラー補足使う側
listenport(0 = 選ぶ)・host・kindポートEADDRINUSEkind: raw(バイト列)・http(旧ランタイムの HTTP サーバー)・それ以外 = TCP だけP, L
unlistenportnull—P, L
listenShareportnullEINVALほかのプロセスから受け取った待ち受けのソケットで accept するP
tcpConnectport・host{ fd, self }ECONNREFUSEDraw の待ち受けにだけつなぐ。self: 呼んだスレッド自身が受け付けるP
tcpConnectOuthost・port・tlsfd—外のホストへの接続(外への接続)P
tcpSetTlsfd・servername外への接続なら true—P
tlsHellofd・certs{ wait, certs }—2 つのプロセスのあいだで証明書を交換するP
shutdownfdnullEBADF・ENOTSOCK片側を閉じる。相手は EOF を読むP
unixListenpソケットの inode の番号EADDRINUSE・ENOENT・ENOTDIRソケットのファイルを作るP
unixUnlisteninonull—ソケットのファイルが同じ inode のままなら消すP
unixConnectpfdECONNREFUSED・ENOENTP
portInfoportそこで待ち受けているか—L
udpBindport・address・reuseポートEADDRINUSEP
udpJoinportnullEINVAL割り当て済みのポートに加わる(共有のソケット)P
udpRecvport・onnull—受信を始める・止めるP
udpCloseportnull—P
udpSendport・from・fromAddress とバイト列null—誰も割り当てていなければ失われるP

エラーと errno の値

エラーは、コードの文字列とメッセージで通信路を通ります。移植ランタイムはこれを libuv のエラー(UVException、負の UV_* の番号)に、旧ランタイムは errno = Linux の番号の負の値の Node 風のエラーにします。E で始まらないコード(ERR_FS_EISDIR)は、その code を持つふつうの Error のままです。カーネルの中でそれ以外の例外が起きたときは、その例外の code、無ければ EIO を返します。

コード番号メッセージ
EPERM1operation not permitted
ENOENT2no such file or directory
ESRCH3no such process
EINTR4eintr
EIO5eio
ENXIO6enxio
E2BIG7e2big
ENOEXEC8exec format error
EBADF9bad file descriptor
ECHILD10no child processes
EAGAIN11resource temporarily unavailable
ENOMEM12enomem
EACCES13permission denied
EFAULT14efault
EBUSY16resource busy or locked
EEXIST17file already exists
EXDEV18cross-device link not permitted
ENODEV19enodev
ENOTDIR20not a directory
EISDIR21illegal operation on a directory
EINVAL22invalid argument
ENFILE23enfile
EMFILE24too many open files
ENOTTY25inappropriate ioctl for device
EFBIG27efbig
ENOSPC28no space left on device
ESPIPE29invalid seek
EROFS30read-only file system
EMLINK31emlink
EPIPE32broken pipe
ERANGE34erange
ENAMETOOLONG36name too long
ENOSYS38function not implemented
ENOTEMPTY39directory not empty
ELOOP40too many symbolic links encountered
ENOTSOCK88socket operation on non-socket
ENOTSUP95operation not supported on socket
EADDRINUSE98address already in use
EADDRNOTAVAIL99eaddrnotavail
ENETUNREACH101enetunreach
ECONNRESET104connection reset by peer
ENOTCONN107socket is not connected
ETIMEDOUT110connection timed out
ECONNREFUSED111connection refused
EHOSTUNREACH113ehostunreach
ECANCELED125ecanceled

表は src/shared/errno.ts のものです。メッセージの無いコードは、コードを小文字にしたものを使います。カーネルが自分でメッセージを付けるエラーもあります: <cmd>: command not found(ENOENT)、<cmd>: Permission denied(EACCES)、 <name>: builtin missing(ENOENT)、spawn: cwd <path> does not exist(ENOENT)、 Path is a directory: rm returned EISDIR (is a directory) <path>(ERR_FS_EISDIR)。

仮想ネットワーク

利用者のマシンでは何も開きません。ポートはカーネルの表の項目で、接続はつながった 2 つのカーネルのソケットです。

TCP のポート

  • ポートの名前空間は、カーネル全体で 1 つです。待ち受けのアドレスは記録しますが、区別には使いません。 127.0.0.1:3000 と ::1:3000 で待ち受けるとぶつかり(EADDRINUSE)、どのループバックのアドレスへの接続もその待ち受けに届きます。
  • ポート 0 は、49152〜65151 の空いているポートを選びます(49152 + random(16000))。
  • すでに待ち受けのあるポートは EADDRINUSE です。同じプロセスでも同じです。
  • 待ち受けの種類:
種類使う側接続ホストに知らせるか
raw移植ランタイム(net・http など)socketpair の上のバイト列知らせる(server-ready・ports())
http旧ランタイムの http.ServerHTTP のメッセージ(http-req など)知らせる
TCP だけ旧ランタイムの net.Server—(プレビューからは ECONNREFUSED)知らせない
  • 待ち受けを始めると(種類 raw と http)、ホストに server-ready { port, pid } を送ります。閉じたとき、またはプロセスが終わったときは port-closed { port } です。
  • execve すると、そのプロセスの待ち受けは無くなります(Node は stdio 以外のすべての fd に close-on-exec を付けます)。
  • 待ち受けのソケットをほかのプロセスへ送ると(child.send(msg, server))共有になります。受け取ったプロセスは listenShare を呼び、接続は動いている持ち主のあいだで順番に配られます。持ち主が終わると、次の受け手がポートを引き継ぎます。

接続

tcpConnect は socketpair を作ります。待ち受けているプロセスは片方の端を新しい fd(空いている 3 以上の最小の番号)と tcp-accept のメッセージで受け取り、呼んだ側はもう片方を受け取ります。socketpair は、ブロックしない書き手に対して向きごとに 212992 バイトまでためます(パイプ)。アドレスの細部:

接続元サーバーから見た remoteAddress / remotePort
プロセス(tcpConnect)127.0.0.1 / 0
プレビュー、または旧ランタイムの HTTP の要求127.0.0.1 / 40001, 40002, …(60000 の次は 40000 に戻る)
Unix ドメインソケット'' / 0

プレビューからの HTTP

プレビューの iframe の要求は、HTTP のメッセージとしてカーネルに届きます(プレビュー)。raw の待ち受けには、カーネル(src/kernel/tcp.ts)が接続を開いて HTTP/1.1 で話します。

  • 要求のヘッダーのうち connection・keep-alive・transfer-encoding・upgrade・sec-websocket-key・ sec-websocket-version は取り除きます。Host が無ければ Host: localhost:<port> を足します。
  • ふつうの要求には Connection: close を付けます。本文があって Content-Length の無い要求は Transfer-Encoding: chunked で送ります。
  • WebSocket の要求には Connection: Upgrade・Upgrade: websocket・Sec-WebSocket-Version: 13 と、ランダムな Sec-WebSocket-Key を付けます。
  • 応答は読み解きます。101 以外の 1xx は読み飛ばします。本文の終わりは、Content-Length・chunked (トレーラーは読んで捨てる)・接続の終わりのどれかで決めます。204 と 304 は本文がありません。応答のヘッダーから transfer-encoding・connection・keep-alive を取り除きます。
  • エラー: 応答の頭が来る前に接続が閉じた → socket hang up。本文が途中で切れた → incomplete response。誰も待ち受けていない → connect ECONNREFUSED 127.0.0.1:<port>。
  • 101 Switching Protocols のあとは、接続に WebSocket のフレームが流れます。テキストとバイナリのメッセージはそのまま渡し、ping には pong で答え、close のフレームで終わります(コードが無ければ 1005)。close のフレーム無しに接続が終わったときはコード 1006 を知らせます。

旧ランタイムの http の待ち受けには、要求を http-req のメッセージとして待ち受けているスレッドへ送ります。その前にプロセスが終わると、要求は server process exited で失敗します。

Unix ドメインソケット

  • listen(path) は、そのパスにソケットのファイル(S_IFSOCK、mode 0o755)を作ります。名前がすでにあれば EADDRINUSE です。
  • 待ち受けはソケットの inode で区別するので、ファイルの名前を変えてもかまいません。シンボリックリンク越しにもつながります。
  • connect(path) は、ファイルがソケットでないとき、または誰も待ち受けていないとき ECONNREFUSED です。
  • サーバーを閉じると、ソケットのファイルが同じ inode のままなら消します。プロセスが終わるか execve すると、そのプロセスの Unix の待ち受けは無くなります。

UDP

  • ポートの名前空間は別です(UDP のポートは TCP のポートとぶつかりません)。
  • ポート 0 に割り当てると、49152〜65151 の空いているポートを選びます。使われているポートは EADDRINUSE ですが、 reuseAddr を付ければ、プロセスどうしで共有します。
  • データグラムはポートだけで届け先を決めます。誰も割り当てていないポートへのデータグラムは失われます。割り当て済みのポートへのものは、受信しているメンバーが現れるまで最大 1024 個まで列で待ち、受信しているメンバーに順番に配ります。
  • 送り手のアドレスは割り当てたアドレスです。無いとき、または 0.0.0.0 のときは 127.0.0.1 です。

ループバックと代わりのアドレス

何がローカルかは、移植ランタイムが決めます(src/nodejs/bindings/net.ts)。

  • connect でループバックとみなすもの: localhost・127.*・::1・0.0.0.0・::・空・::ffff:127.0.0.1。これらは仮想ポートへ行き(tcpConnect)、それ以外のアドレスは外へ行きます(tcpConnectOut)。
  • dns.lookup('localhost') と、.localhost で終わる名前は 127.0.0.1 と ::1 になります。IP アドレスはそのまま返します。
  • それ以外の名前には、198.18.0.0/15 から代わりの IPv4 アドレスを付けます。最初に引いた順に(プロセスごとに) 198.18.0.1・198.18.0.2・… です。代わりのアドレスに接続すると、もとの名前がカーネルに渡り、要求はそのホストへ出ていきます。そうした名前の IPv6 は見つかりません(ENOTFOUND)。resolve4/resolveAny は代わりのアドレスを返します。

外への接続

ブラウザはソケットを開けません。プロセスがループバック以外のアドレスに TCP でつなぐと(tcpConnectOut)、プロセスは socketpair の片方の端を受け取ります。カーネルはもう片方の端から HTTP/1.1 の要求を読み(src/kernel/outbound.ts)、1 つずつ、ランタイムの Service Worker の方針を通すプロキシ(<RUNTIME_BASE>/__ov/net)経由の fetch で出し、応答を HTTP/1.1 のバイト列として書き戻します。このため、自前の HTTP クライアントを持つライブラリ(undici・axios・got)も、そのまま動きます。

要求の読み取り

決まりふるまい
最初のバイト要求の行に見えること(/^[A-Z]{3,10} /)。それ以外(別のプロトコル、本物の TLS のハンドシェイク)は、応答せずに接続を閉じる
要求の行METHOD target HTTP/1.0 または HTTP/1.1。違えば 400 Bad Request を返して閉じる
頭の大きさ空行まで最大 1 MiB。超えたら接続を閉じる
本文Transfer-Encoding: chunked(拡張とトレーラーは無視)か Content-Length。どちらも無ければ空
keep-aliveHTTP/1.1 は Connection: close が無ければ続ける。HTTP/1.0 は Connection: keep-alive のときだけ。1 本の接続の要求は 1 つずつ順に処理する
WebSocketUpgrade: websocket には 501 Not Implemented を返して閉じる
URL絶対形式の target はそのまま使う。そうでなければ http(s):// + Host ヘッダー(無ければ接続先のホスト。80/443 以外は :port 付き)+ パス

転送と応答

  • 要求のヘッダーのうち connection・keep-alive・proxy-connection・transfer-encoding・upgrade・te・trailer・ host・content-length は送りません。Service Worker はさらに削ります(ネットワークの方針)。
  • GET と HEAD はそのまま送り、ほかのメソッドは本当のメソッドを x-ov-method に入れて POST で送ります。許可を求める画面に出る送り主は <pid> <argv[0..2]>(最大 200 文字)です。
  • 応答は HTTP/1.1 <status> <statusText> で書きます(statusText が空なら OK)。fetch が本文をすでに展開しているので content-encoding・content-length・transfer-encoding・connection・keep-alive を取り除き、本文を Transfer-Encoding: chunked で区切り直します。204・304・1xx と HEAD への応答には本文がありません。
  • リダイレクトはブラウザがたどります。

エラー

エラーは errno ではなく HTTP の応答になります。接続そのものは「できた」ことになっているからです。

状況応答
ネットワークの方針がそのホストを拒んだ403 Forbidden
fetch が失敗した(ネットワークのエラー、CORS)、またはプロキシが失敗した502 Bad Gateway

どちらも Content-Type: text/plain・Content-Length・Connection: close・X-OpenVessel-Error: 1 を持ち、本文はメッセージです(たとえば OpenVessel: network access to example.com was denied by the network policy)。そのあと接続は閉じます。外へ出られるのは HTTP/1.1 だけです。ほかのプロトコルや、生のソケットでの外への WebSocket は使えません。

TLS の素通し

通信路の上では TLS をしません。プロセスが外への接続に TLS をかけると(tls.connect、https)、プロセスの TLS の層は TLS を始めるときに tcpSetTls を呼びます。カーネルはその接続を https として記録し(SNI の servername をホストにします)、プロセスの TLS の層は平文をそのまま通します。本物の TLS と証明書の確認は、ブラウザの fetch がします。OpenVessel の中のプロセスどうしでは、TLS の接続の両端が tlsHello で証明書を交換し、バイト列はやはり平文のままです。tls の API への影響は 互換性 を見てください。

ES モジュールの配信

src/kernel/esm.ts は、VFS のファイルをブラウザの ES モジュールとして配ります。ランタイムの Service Worker が次の URL をカーネルへ回し、答えを Content-Type: text/javascript; charset=utf-8 と Cache-Control: no-store で返します。

URL答え
/__ov/esm/<絶対パス>[?query]そのファイルを ES モジュールにしたもの
/__ov/builtin/<name>組み込みのモジュール(node:fs など)を globalThis.__ov.builtin(name) から export し直すモジュール
/__ov/mw/<pid>/<id>.mjsesmRegister で登録したソース(移植ランタイムの ModuleWrap)。知らないキーには ERR_INTERNAL_ASSERTION を投げるモジュール

形式ごとの、ファイルからモジュールへの変え方:

形式モジュール
module(.mjs・"type": "module"・ES の構文と判定したもの)import の指定子を上の URL に書き換え、import.meta → globalThis.__ov.meta(<ファイルの URL>)、import() → globalThis.__ov.dimport(…)、import の属性を取り除き、#! の行をコメントにし、//# sourceURL=file://… を足したソース
module-typescript(.mts、"type": "commonjs" の外の .ts)sucrase で型を除いてから module と同じ
commonjs・commonjs-typescript表向きのモジュール: default = globalThis.__ov.requireForImport(file) の module.exports。cjs-module-lexer で見つけた名前付きの export も付ける(再 export は 8 段までたどる)
json(または with { type: 'json' } のときに付く ?ov-json)export default JSON.parse(…)
addon(.node)エラー ERR_DLOPEN_FAILED
wasmエラー ERR_UNKNOWN_FILE_EXTENSION
ファイルが無い / ディレクトリERR_MODULE_NOT_FOUND / ERR_UNSUPPORTED_DIR_IMPORT

モジュールを作れないときは、そのコードの Error を投げるモジュールで答えるので、エラーは import したところで表に出ます。作ったモジュールはパスごとに、inode の番号・mtime・大きさをキーにして覚えておきます。4000 個を超えたらすべて捨てます。esmRegister で登録したソースは、そのプロセスが終わると捨てます。旧ランタイムは ES モジュールを /__ov/esm/ から読み、移植ランタイムは自分でコンパイルするソースを登録して /__ov/mw/ から読みます。

組み込みのプログラム

登録されているプログラム

組み込みのプログラムは名前で登録し、#!/ov/builtin <name> と書いた実行できる小さなファイル(mode 0o755)として置きます。PATH にファイルが見つからなくても、登録された名前と同じコマンド名なら、その組み込みを動かします。

ディレクトリプログラム
/binsh・bash・dash(同じシェル。シェル)
/binls・cat・mkdir・mkfifo・rm・rmdir・cp・mv・touch・ln・chmod・head・tail・wc・grep・egrep・fgrep・find・sort・uniq・cut・tr・sed・xargs・tee・env・which・basename・dirname・realpath・readlink・stat・du・df・free・date・sleep・uname・ps・kill・seq・yes・mktemp・base64・diff・clear・nproc・id・hostname・whoami・tty・stty・nohup・nice・less・more・file・rev・nl・timeout・time・truncate・sha256sum・sha1sum・sha512sum・md5sum・echo・printf・test・[・true・false・pwd・chown(何もしない)・sync(何もしない)
/usr/binenv(#!/usr/bin/env の行のための 2 つめ)
/usr/binnode(#!/ov/builtin node。node のプロセスを起動する)と nodejs(/usr/bin/node へのシンボリックリンク)
/usr/binnpm・pnpm・yarn・bun・npx・pnpx・bunx・corepack(パッケージマネージャ)
/usr/bincurl・wget

curl と wget

curl と wget は、npm と同じ、方針を通すプロキシで取りに行きます。curl で使えるオプション:

オプション効果
-X・--request <method>要求のメソッド(既定 GET)
-o・--output <file>本文をファイルに書く
-O・--remote-nameURL のパスの最後の部分に書く(空なら index.html)
-H・--header '<name>: <value>'要求のヘッダーを足す
-d・--data・--data-raw・--data-binary <data>要求の本文。GET を POST に変える
-I・--headHEAD の要求。状態の行とヘッダーだけ出す
-i・--include本文の前に状態の行とヘッダーを出す
-f・--fail状態が 400 以上なら curl: (22) The requested URL returned error: <status> を出して 22 で終わる
-s・--silent、sSLfk を組み合わせた短いフラグ受け付ける(組み合わせの中の f は --fail)
-L・--location・-k・--insecure・--compressed受け付ける。リダイレクトはいつもたどる

スキームの無い URL には http:// を付けます。終了コード: 0 成功、2 URL が無い(curl: no URL specified!)、 7 要求の失敗(curl: (7) <message>。方針による拒否も含む)、22 --fail のとき。wget <url> は URL のパスの最後の部分(空なら index.html)に保存し、wget -O <file> <url> は <file> に、-O - は標準出力に書きます。ふるまいは curl -fsSL と同じです。

起動時のファイルシステム

起動したときにカーネルが作るもの(src/kernel/kernel.ts・src/kernel/index.ts・src/kernel/coreutils.ts・ src/kernel/npm/cli.ts・src/kernel/curl.ts):

パス種類中身 / mode
/bin・/usr/bin・/usr/local/bin・/usr/lib/node_modules・/usr/local/lib・/home/user・/etc・/dev・/proc・/var/tmp・/root・/optディレクトリ0755(親の /usr・/usr/local・/usr/lib・/home・/var も)
/tmpディレクトリ1777
/etc/passwdファイルroot:x:0:0:root:/root:/bin/sh、user:x:1000:1000:user:/home/user:/bin/sh、nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
/etc/groupファイルroot:x:0:、user:x:1000:、nogroup:x:65534:
/etc/hostnameファイルopenvessel
/etc/hostsファイル127.0.0.1 localhost、::1 localhost
/etc/os-releaseファイルNAME="OpenVessel"、ID=openvessel、PRETTY_NAME="OpenVessel"
/etc/shellsファイル/bin/sh、/bin/bash
/dev/null・/dev/zero・/dev/random・/dev/urandomキャラクタデバイス0666
/dev/fdディレクトリ空。/dev/fd/N はプロセスごとに答える
/dev/stdin・/dev/stdout・/dev/stderrシンボリックリンク/proc/self/fd/0・/1・/2 へ
/proc/cpuinfoファイルprocessor : 0、model name : OpenVessel virtual CPU
/proc/meminfoファイルMemTotal: 4194304 kB、MemFree: 2097152 kB
/proc/versionファイルLinux version 6.1.0-openvessel
/bin/*・/usr/bin/*実行できる小さなファイル登録されているプログラム を参照
/usr/bin/nodejsシンボリックリンク/usr/bin/node へ
/home/user/.shrcファイルalias ll='ls -l'、alias la='ls -la'

通常のファイルは mode 0644 で作ります。/etc/openvessel/node は起動時にはありません(旧ランタイムを使います)。ホストが置くファイルは、既定で /home/user/app に入ります(ホスト API)。ホスト名は openvessel です。

既定の環境変数

kernel.defaultEnv() は、ホストが起動するプロセスの環境変数(ホストが渡す env と合わせたもの)と、親の無いプロセスの環境変数です。

変数値
PATH/usr/local/bin:/usr/bin:/bin
HOME/home/user
USERuser
LOGNAMEuser
SHELL/bin/sh
TERMxterm-256color
LANGen_US.UTF-8
TMPDIR/tmp
HOSTNAMEopenvessel
NAPI_RS_FORCE_WASI1(napi-rs のパッケージが WebAssembly 版を読む)

子プロセスは、呼び出し側が渡さない限り、親の環境変数の写しを受け継ぎます。シェルは自分の変数を足します(PWD など。シェル)。

この文書の元は、リポジトリの docs/reference/kernel.ja.md です(英語版は kernel.md)。