mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-02 07:34:45 +08:00
* feat(isolation): add RFC 0012 backend contract Signed-off-by: Drew Newberry <385+drew@users.noreply.github.com> * refactor(isolation): name the interface crate explicitly Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(isolation): expose trusted host gateway Signed-off-by: Drew Newberry <anewberry@nvidia.com> * docs(agents): inventory the MXC driver Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(isolation): add mediated DNS transport Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): tighten interface error and digest contracts Signed-off-by: Drew Newberry <anewberry@nvidia.com> * docs(isolation): remove unrelated driver inventory Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(isolation): define capability-free launch contract Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): seal confirmed boundary state Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): validate confirmation for external backend implementations Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): clarify mediated DNS identity Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(isolation): generalize loopback connector Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(isolation): unify typed network mediation Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(isolation): bind launches to sandbox sessions Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(mxc): initialize extended sandbox status Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(isolation): add boundary protocol and Linux primitives Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): harden signals and separate process status from transport Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): validate remote confirmation through public contract Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): validate wire state and propagate snapshot failures Signed-off-by: Drew Newberry <anewberry@nvidia.com> * test(isolation): import owned agent specification explicitly Signed-off-by: Drew Newberry <anewberry@nvidia.com> * docs(isolation): describe mediated DNS channel Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): bound mediation attach without nested retries Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(isolation): generalize loopback protocol Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(isolation): add transport-neutral session authentication Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(isolation): separate sandbox backend protocol Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): harden runtime boundary controls Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(isolation): add terminal boundary operation Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(isolation): split supervisor and sandbox runtimes Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(sandbox): harden boundary isolation and lifecycle ownership Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(sandbox): reject private root redirects and adopt typed errors Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(sandbox): preserve accept thread ownership on musl Signed-off-by: Drew Newberry <anewberry@nvidia.com> * test(sandbox): isolate credential probes from filtered threads Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(sandbox): return retained exec exit status to independent waiters Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(sandbox): bound network mediation and preserve socket authorization Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(sandbox): bound control admission and retire stale mediation Signed-off-by: Drew Newberry <anewberry@nvidia.com> * ci(e2e): select migrated drivers per stack layer Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(sandbox): implement loopback connector Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(isolation): authenticate the Sandbox Protocol Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(supervisor): rotate launch-scoped authentication Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(sandbox): consume dedicated backend crate Signed-off-by: Drew Newberry <anewberry@nvidia.com> * test(sandbox): align topology session fixture Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(sandbox): align projected bootstrap bundle Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(auth): validate refreshed credentials before rotation Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(sandbox): fail closed across supervisor disconnects Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): repair rebased sandbox CI Signed-off-by: Drew Newberry <anewberry@nvidia.com> * build(runtime): publish separate sandbox and supervisor images Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(config): configure the sandbox runtime image Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(ci): validate sandbox binary linkage Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(isolation): use backend and runtime terminology Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(sandbox): use a scratch runtime image Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(ci): refresh schema and dependency policy Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(sandbox): bind reconnects to supervisor process Signed-off-by: Drew Newberry <anewberry@nvidia.com> * docs: align runtime split operational guidance Signed-off-by: Drew Newberry <anewberry@nvidia.com> * chore(security): document Kubernetes runtime RBAC Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): enforce runtime lifecycle invariants Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(compute): identify sandbox start generations Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(server): restore sandbox launch sessions Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): support authenticated runtime replacement Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(auth): bind sandbox session successors Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(auth): retry pending sandbox successors Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(vm): run the supervisor outside the guest workload Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(vm): use sandbox backend protocol Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): repair rebase integration Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): use unified build toolchain Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(vm): use sandbox runtime terminology Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(vm): own guest network bootstrap Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): expose guest init version Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): select native supervisor artifacts Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): guard guest init Linux symbols Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): scope Linux test imports Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): avoid guest interface casts Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): reconcile admitted sandbox identity Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): share resolved sandbox identity Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): surface host supervisor failures Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): include guest logs on supervisor exit Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): rotate and clean runtime generations Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): make sandbox starts generation-aware Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): rotate restored sandbox sessions Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(vm): keep shared paths in the base layer Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): bind sandbox session lineage Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(docker): isolate workloads behind the host supervisor Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(docker): rotate launch-scoped authentication Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(docker): use sandbox backend protocol Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(docker): use host networking for supervisor Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(docker): preserve host gateway alias resolution Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(docker): use separate sandbox and supervisor images Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(docker): restore startup validation after rebase Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(docker): name the sandbox runtime directly Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(docker): narrow supervisor CA runtime storage Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(docker): close companion isolation gaps Signed-off-by: Drew Newberry <anewberry@nvidia.com> * test(docker): align mediated network expectations Signed-off-by: Drew Newberry <anewberry@nvidia.com> * test(docker): exercise mediated network paths Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(docker): attach supervisor to managed network Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(docker): defer supervisor recovery until gateway is ready Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(docker): make sandbox starts generation-aware Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(docker): rotate restored sandbox sessions Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(docker): preserve workloads during session rotation Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(docker): remove unrelated configuration RFC changes Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(docker): bind sandbox session lineage Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(kubernetes): add proxy-pod isolation topology Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(kubernetes): use sandbox backend protocol Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): use stable sandbox service authority Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(kubernetes): split sandbox and supervisor images Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): adapt proxy pods to current runtime APIs Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(kubernetes): describe the single runtime placement Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(kubernetes): simplify sandbox orchestration Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): validate deployment prerequisites Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): update Trivy Helm profile inventory Signed-off-by: Drew Newberry <anewberry@nvidia.com> * test(kubernetes): update Trivy scan inventory count Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): reuse preloaded runtime images in e2e Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): type and clean runtime resources Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): make sandbox restarts recoverable Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): rotate restored sandbox sessions Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): preserve supervisor egress Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): bind sandbox session lineage Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(podman): adopt isolated sandbox and supervisor containers Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): stage bootstrap archives at named volume destinations Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(podman): rotate launch-scoped authentication Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(podman): use sandbox backend protocol Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(podman): use host networking for supervisor Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(podman): split sandbox and supervisor images Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): repair rebase integration Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(podman): name the sandbox runtime directly Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): provision supervisor CA runtime storage Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): address isolation review findings Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): inspect Debian supervisor provenance Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): use libpod-compatible tmpfs options Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): bind verified sandbox runtime binary Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): provide external driver data directory Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): start sandbox before joining user namespace Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): separate supervisor user namespace Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): make sandbox starts generation-aware Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): rotate restored sandbox sessions Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): bind sandbox session lineage Signed-off-by: Drew Newberry <anewberry@nvidia.com> * perf(isolation): add TCP and DNS benchmark harnesses Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(perf): align benchmark timing and supported protocols Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(perf): report TCP benchmark metrics accurately Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(perf): cancel failed worker startup Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(docker): build matching local supervisor image Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): make local sandbox smoke test runnable Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): wire local sandbox runtime image Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): narrow sandbox service RBAC Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(ci): validate split runtime artifacts Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): harden runtime session handling Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(supervisor): add standalone network proxy role Signed-off-by: Drew Newberry <anewberry@nvidia.com> * docs(rfc): remove implementation companion notes Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(vm): standardize runtime release name Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): pin renamed runtime artifacts Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(auth): persist sandbox runtime identity Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(runtime): restore branch validation Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): reconcile main after rebase Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(network): close unframed HTTP 1.0 responses Signed-off-by: Drew Newberry <anewberry@nvidia.com> * chore(isolation): preserve upstream OCSF updates Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(security): close credential and TLS replay paths Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(auth): make sandbox refresh retries idempotent Signed-off-by: Drew Newberry <anewberry@nvidia.com> --------- Signed-off-by: Drew Newberry <385+drew@users.noreply.github.com> Signed-off-by: Drew Newberry <anewberry@nvidia.com>
499 lines
16 KiB
Rust
499 lines
16 KiB
Rust
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
//! E2E tests for JSON-RPC L7 inspection through transparent interception.
|
|
//!
|
|
//! The upstream server deliberately does not implement JSON-RPC. `OpenShell`
|
|
//! parses and enforces JSON-RPC before forwarding, so any HTTP server that
|
|
//! accepts POST /rpc is enough to prove allowed requests reach upstream
|
|
//! and denied requests are stopped by the sandbox proxy.
|
|
|
|
#![cfg(feature = "e2e")]
|
|
|
|
use std::io::Write;
|
|
|
|
use openshell_e2e::harness::container::ContainerHttpServer;
|
|
use openshell_e2e::harness::sandbox::SandboxGuard;
|
|
use tempfile::NamedTempFile;
|
|
|
|
const RULES_TEST_SERVER_ALIAS: &str = "jsonrpc-l7-rules.openshell.test";
|
|
const AUDIT_TEST_SERVER_ALIAS: &str = "jsonrpc-l7-audit.openshell.test";
|
|
|
|
async fn start_test_server(alias: &str) -> Result<ContainerHttpServer, String> {
|
|
let script = r#"from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def read_body(self):
|
|
if self.headers.get("Transfer-Encoding", "").lower() == "chunked":
|
|
data = b""
|
|
while True:
|
|
size_line = self.rfile.readline()
|
|
if not size_line:
|
|
break
|
|
size = int(size_line.split(b";", 1)[0].strip(), 16)
|
|
if size == 0:
|
|
while self.rfile.readline().strip():
|
|
pass
|
|
break
|
|
data += self.rfile.read(size)
|
|
self.rfile.read(2)
|
|
return data
|
|
return self.rfile.read(int(self.headers.get("Content-Length", "0")))
|
|
|
|
def do_GET(self):
|
|
self.send_response(200)
|
|
self.end_headers()
|
|
|
|
def do_POST(self):
|
|
self.read_body()
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.end_headers()
|
|
self.wfile.write(b'{"jsonrpc":"2.0","id":1,"result":{}}')
|
|
|
|
def log_message(self, format, *args):
|
|
pass
|
|
|
|
HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()
|
|
"#;
|
|
|
|
ContainerHttpServer::start_python(alias, script).await
|
|
}
|
|
|
|
fn write_jsonrpc_policy(host: &str, port: u16) -> Result<NamedTempFile, String> {
|
|
let mut file = NamedTempFile::new().map_err(|e| format!("create temp policy file: {e}"))?;
|
|
let policy = format!(
|
|
r#"version: 1
|
|
|
|
filesystem_policy:
|
|
include_workdir: true
|
|
read_only:
|
|
- /usr
|
|
- /lib
|
|
- /proc
|
|
- /dev/urandom
|
|
- /app
|
|
- /etc
|
|
- /var/log
|
|
read_write:
|
|
- /sandbox
|
|
- /tmp
|
|
- /dev/null
|
|
|
|
landlock:
|
|
compatibility: best_effort
|
|
|
|
process:
|
|
run_as_user: sandbox
|
|
run_as_group: sandbox
|
|
|
|
network_policies:
|
|
test_jsonrpc_l7:
|
|
name: test_jsonrpc_l7
|
|
endpoints:
|
|
- host: {host}
|
|
port: {port}
|
|
path: /rpc
|
|
protocol: json-rpc
|
|
enforcement: enforce
|
|
allowed_ips:
|
|
- "10.0.0.0/8"
|
|
- "172.0.0.0/8"
|
|
- "192.168.0.0/16"
|
|
- "fc00::/7"
|
|
json_rpc:
|
|
max_body_bytes: 65536
|
|
rules:
|
|
- allow:
|
|
method: initialize
|
|
- allow:
|
|
method: tools/list
|
|
- allow:
|
|
method: tools/call
|
|
deny_rules:
|
|
- method: tools/delete
|
|
binaries:
|
|
- path: /usr/bin/python*
|
|
- path: /usr/local/bin/python*
|
|
- path: /sandbox/.uv/python/*/bin/python*
|
|
"#
|
|
);
|
|
file.write_all(policy.as_bytes())
|
|
.map_err(|e| format!("write temp policy file: {e}"))?;
|
|
file.flush()
|
|
.map_err(|e| format!("flush temp policy file: {e}"))?;
|
|
Ok(file)
|
|
}
|
|
|
|
fn write_jsonrpc_default_audit_policy(host: &str, port: u16) -> Result<NamedTempFile, String> {
|
|
let mut file = NamedTempFile::new().map_err(|e| format!("create temp policy file: {e}"))?;
|
|
let policy = format!(
|
|
r#"version: 1
|
|
|
|
filesystem_policy:
|
|
include_workdir: true
|
|
read_only:
|
|
- /usr
|
|
- /lib
|
|
- /proc
|
|
- /dev/urandom
|
|
- /app
|
|
- /etc
|
|
- /var/log
|
|
read_write:
|
|
- /sandbox
|
|
- /tmp
|
|
- /dev/null
|
|
|
|
landlock:
|
|
compatibility: best_effort
|
|
|
|
process:
|
|
run_as_user: sandbox
|
|
run_as_group: sandbox
|
|
|
|
network_policies:
|
|
test_jsonrpc_l7_audit:
|
|
name: test_jsonrpc_l7_audit
|
|
endpoints:
|
|
- host: {host}
|
|
port: {port}
|
|
path: /rpc
|
|
protocol: json-rpc
|
|
allowed_ips:
|
|
- "10.0.0.0/8"
|
|
- "100.64.0.0/10"
|
|
- "172.0.0.0/8"
|
|
- "198.18.0.0/15"
|
|
- "192.168.0.0/16"
|
|
- "fc00::/7"
|
|
json_rpc:
|
|
max_body_bytes: 65536
|
|
rules:
|
|
- allow:
|
|
method: initialize
|
|
binaries:
|
|
- path: /usr/bin/python*
|
|
- path: /usr/local/bin/python*
|
|
- path: /sandbox/.uv/python/*/bin/python*
|
|
"#
|
|
);
|
|
file.write_all(policy.as_bytes())
|
|
.map_err(|e| format!("write temp policy file: {e}"))?;
|
|
file.flush()
|
|
.map_err(|e| format!("flush temp policy file: {e}"))?;
|
|
Ok(file)
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[allow(clippy::too_many_lines)]
|
|
async fn jsonrpc_l7_enforces_high_level_and_raw_transparent_paths() {
|
|
let server = start_test_server(RULES_TEST_SERVER_ALIAS)
|
|
.await
|
|
.expect("start test server");
|
|
let policy = write_jsonrpc_policy(&server.host, server.port).expect("write custom policy");
|
|
let policy_path = policy
|
|
.path()
|
|
.to_str()
|
|
.expect("temp policy path should be utf-8")
|
|
.to_string();
|
|
|
|
let script = format!(
|
|
r#"
|
|
import json
|
|
import socket
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
HOST = {host:?}
|
|
PORT = {port}
|
|
DETAILS = {{
|
|
"debug_target": {{"host": HOST, "port": PORT}},
|
|
}}
|
|
|
|
def text(data):
|
|
return data.decode(errors="replace")
|
|
|
|
def selected_headers(headers):
|
|
return {{
|
|
key.lower(): value
|
|
for key, value in headers.items()
|
|
if key.lower() in ("content-type", "content-length", "server")
|
|
}}
|
|
|
|
def record_http_error(label, error, request_body):
|
|
response_body = error.read()
|
|
DETAILS[f"{{label}}_request"] = request_body
|
|
DETAILS[f"{{label}}_response"] = {{
|
|
"status": error.code,
|
|
"reason": str(error.reason),
|
|
"headers": selected_headers(error.headers),
|
|
"body": text(response_body),
|
|
}}
|
|
return error.code
|
|
|
|
def post_jsonrpc(label, method, params=None, req_id=1):
|
|
body = {{"jsonrpc": "2.0", "id": req_id, "method": method}}
|
|
if params is not None:
|
|
body["params"] = params
|
|
encoded = json.dumps(body).encode()
|
|
request = urllib.request.Request(
|
|
f"http://{{HOST}}:{{PORT}}/rpc",
|
|
data=encoded,
|
|
headers={{"Content-Type": "application/json"}},
|
|
method="POST",
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=15) as response:
|
|
response.read()
|
|
return response.status
|
|
except urllib.error.HTTPError as error:
|
|
return record_http_error(label, error, body)
|
|
|
|
def post_jsonrpc_batch(label, requests):
|
|
encoded = json.dumps(requests).encode()
|
|
request = urllib.request.Request(
|
|
f"http://{{HOST}}:{{PORT}}/rpc",
|
|
data=encoded,
|
|
headers={{"Content-Type": "application/json"}},
|
|
method="POST",
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=15) as response:
|
|
response.read()
|
|
return response.status
|
|
except urllib.error.HTTPError as error:
|
|
return record_http_error(label, error, requests)
|
|
|
|
def post_invalid_json(label):
|
|
encoded = b"not valid json {{"
|
|
request = urllib.request.Request(
|
|
f"http://{{HOST}}:{{PORT}}/rpc",
|
|
data=encoded,
|
|
headers={{"Content-Type": "application/json", "Content-Length": str(len(encoded))}},
|
|
method="POST",
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=15) as response:
|
|
response.read()
|
|
return response.status
|
|
except urllib.error.HTTPError as error:
|
|
return record_http_error(label, error, text(encoded))
|
|
|
|
def read_until(sock, marker):
|
|
data = b""
|
|
while marker not in data:
|
|
chunk = sock.recv(4096)
|
|
if not chunk:
|
|
break
|
|
data += chunk
|
|
return data
|
|
|
|
def read_response(sock):
|
|
response = read_until(sock, b"\r\n\r\n")
|
|
headers, _, body = response.partition(b"\r\n\r\n")
|
|
content_length = 0
|
|
for line in headers.split(b"\r\n")[1:]:
|
|
if line.lower().startswith(b"content-length:"):
|
|
content_length = int(line.split(b":", 1)[1].strip())
|
|
break
|
|
while len(body) < content_length:
|
|
chunk = sock.recv(4096)
|
|
if not chunk:
|
|
break
|
|
body += chunk
|
|
return response, body
|
|
|
|
def status_code(response, label):
|
|
parts = response.split()
|
|
if len(parts) < 2:
|
|
DETAILS[f"{{label}}_raw"] = response.decode(errors="replace")
|
|
raise RuntimeError(f"{{label}}: malformed HTTP response: {{response!r}}")
|
|
try:
|
|
return int(parts[1])
|
|
except ValueError as error:
|
|
DETAILS[f"{{label}}_raw"] = response.decode(errors="replace")
|
|
raise RuntimeError(f"{{label}}: non-numeric HTTP status: {{response!r}}") from error
|
|
|
|
def record_raw_response(label, response, body=b""):
|
|
code = status_code(response, label)
|
|
if code != 200:
|
|
DETAILS[f"{{label}}_raw"] = text(response)
|
|
if body:
|
|
DETAILS[f"{{label}}_body"] = text(body)
|
|
return code
|
|
|
|
def raw_http_status(label, request):
|
|
last_error = None
|
|
for attempt in range(5):
|
|
try:
|
|
with socket.create_connection((HOST, PORT), timeout=15) as sock:
|
|
sock.sendall(request)
|
|
sock.shutdown(socket.SHUT_WR)
|
|
response, body = read_response(sock)
|
|
return record_raw_response(f"{{label}}_response", response, body)
|
|
except (OSError, RuntimeError) as error:
|
|
last_error = error
|
|
DETAILS[f"{{label}}_attempt_{{attempt + 1}}_error"] = str(error)
|
|
time.sleep(0.2)
|
|
|
|
raise RuntimeError(f"{{label}}: failed after 5 attempts: {{last_error}}")
|
|
|
|
def raw_jsonrpc_status(method, params, label):
|
|
target = f"{{HOST}}:{{PORT}}"
|
|
body = {{"jsonrpc": "2.0", "id": 1, "method": method}}
|
|
if params is not None:
|
|
body["params"] = params
|
|
encoded = json.dumps(body).encode()
|
|
request = (
|
|
f"POST /rpc HTTP/1.1\r\n"
|
|
f"Host: {{target}}\r\n"
|
|
f"Content-Type: application/json\r\n"
|
|
f"Content-Length: {{len(encoded)}}\r\n"
|
|
f"Connection: close\r\n"
|
|
f"\r\n"
|
|
).encode() + encoded
|
|
return raw_http_status(label, request)
|
|
|
|
results = {{
|
|
# forward proxy — method-only allow rules
|
|
"forward_method_initialize_allowed": post_jsonrpc("forward_method_initialize_allowed", "initialize", {{"protocolVersion": "2025-11-25", "capabilities": {{}}}}),
|
|
"forward_method_tools_list_allowed": post_jsonrpc("forward_method_tools_list_allowed", "tools/list"),
|
|
|
|
# forward proxy — method allow/deny rules
|
|
"forward_method_tools_call_allowed": post_jsonrpc("forward_method_tools_call_allowed", "tools/call", {{"name": "read_status"}}),
|
|
"forward_method_tools_call_with_unmatched_params_allowed": post_jsonrpc("forward_method_tools_call_with_unmatched_params_allowed", "tools/call", {{"name": "blocked_action", "arguments": {{"scope": "ignored"}}}}),
|
|
"forward_method_tools_delete_denied": post_jsonrpc("forward_method_tools_delete_denied", "tools/delete", {{"name": "purge_cache"}}),
|
|
|
|
# forward proxy — batch: all requests allowed
|
|
"forward_batch_all_allowed": post_jsonrpc_batch("forward_batch_all_allowed", [
|
|
{{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}},
|
|
{{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {{"name": "read_status"}}}},
|
|
]),
|
|
|
|
# forward proxy — batch: one denied request causes full batch denial
|
|
"forward_batch_one_denied": post_jsonrpc_batch("forward_batch_one_denied", [
|
|
{{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}},
|
|
{{"jsonrpc": "2.0", "id": 2, "method": "tools/delete", "params": {{"name": "purge_cache"}}}},
|
|
]),
|
|
|
|
# forward proxy — invalid JSON body fails closed before generic rules apply
|
|
"forward_invalid_json_denied": post_invalid_json("forward_invalid_json_denied"),
|
|
|
|
# raw socket path — representative allowed and denied cases
|
|
"raw_method_initialize_allowed": raw_jsonrpc_status("initialize", {{"protocolVersion": "2025-11-25", "capabilities": {{}}}}, "raw_method_initialize_allowed"),
|
|
"raw_method_tools_list_allowed": raw_jsonrpc_status("tools/list", None, "raw_method_tools_list_allowed"),
|
|
"raw_method_tools_call_allowed": raw_jsonrpc_status("tools/call", {{"name": "read_status"}}, "raw_method_tools_call_allowed"),
|
|
"raw_method_tools_call_with_unmatched_params_allowed": raw_jsonrpc_status("tools/call", {{"name": "blocked_action", "arguments": {{"scope": "ignored"}}}}, "raw_method_tools_call_with_unmatched_params_allowed"),
|
|
"raw_method_tools_delete_denied": raw_jsonrpc_status("tools/delete", {{"name": "purge_cache"}}, "raw_method_tools_delete_denied"),
|
|
}}
|
|
results.update(DETAILS)
|
|
print(json.dumps(results, sort_keys=True))
|
|
"#,
|
|
host = server.host,
|
|
port = server.port,
|
|
);
|
|
|
|
let guard = SandboxGuard::create(&["--policy", &policy_path, "--", "python3", "-c", &script])
|
|
.await
|
|
.expect("sandbox create");
|
|
|
|
for (key, expected) in [
|
|
// forward proxy — allowed
|
|
("forward_method_initialize_allowed", 200),
|
|
("forward_method_tools_list_allowed", 200),
|
|
("forward_method_tools_call_allowed", 200),
|
|
(
|
|
"forward_method_tools_call_with_unmatched_params_allowed",
|
|
200,
|
|
),
|
|
// forward proxy — method denied
|
|
("forward_method_tools_delete_denied", 403),
|
|
// forward proxy — batch
|
|
("forward_batch_all_allowed", 200),
|
|
("forward_batch_one_denied", 403),
|
|
// forward proxy — parse error
|
|
("forward_invalid_json_denied", 403),
|
|
// raw socket path — allowed
|
|
("raw_method_initialize_allowed", 200),
|
|
("raw_method_tools_list_allowed", 200),
|
|
("raw_method_tools_call_allowed", 200),
|
|
("raw_method_tools_call_with_unmatched_params_allowed", 200),
|
|
// raw socket path — method denied
|
|
("raw_method_tools_delete_denied", 403),
|
|
] {
|
|
let expected_fragment = format!(r#""{key}": {expected}"#);
|
|
assert!(
|
|
guard.create_output.contains(&expected_fragment),
|
|
"expected {key}={expected}, got:\n{}",
|
|
guard.create_output
|
|
);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn jsonrpc_forward_proxy_hard_denies_response_frames_in_default_audit_mode() {
|
|
let server = start_test_server(AUDIT_TEST_SERVER_ALIAS)
|
|
.await
|
|
.expect("start test server");
|
|
let policy =
|
|
write_jsonrpc_default_audit_policy(&server.host, server.port).expect("write custom policy");
|
|
let policy_path = policy
|
|
.path()
|
|
.to_str()
|
|
.expect("temp policy path should be utf-8")
|
|
.to_string();
|
|
|
|
let script = format!(
|
|
r#"
|
|
import json
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
HOST = {host:?}
|
|
PORT = {port}
|
|
|
|
def post_jsonrpc(body):
|
|
encoded = json.dumps(body).encode()
|
|
request = urllib.request.Request(
|
|
f"http://{{HOST}}:{{PORT}}/rpc",
|
|
data=encoded,
|
|
headers={{"Content-Type": "application/json"}},
|
|
method="POST",
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=15) as response:
|
|
response.read()
|
|
return response.status
|
|
except urllib.error.HTTPError as error:
|
|
error.read()
|
|
return error.code
|
|
|
|
results = {{
|
|
"forward_unknown_method_audited": post_jsonrpc({{"jsonrpc": "2.0", "id": 1, "method": "unknown/method"}}),
|
|
"forward_response_frame_hard_denied": post_jsonrpc({{"jsonrpc": "2.0", "id": 1, "result": {{}}}}),
|
|
}}
|
|
print(json.dumps(results, sort_keys=True))
|
|
"#,
|
|
host = server.host,
|
|
port = server.port,
|
|
);
|
|
|
|
let guard = SandboxGuard::create(&["--policy", &policy_path, "--", "python3", "-c", &script])
|
|
.await
|
|
.expect("sandbox create");
|
|
|
|
for (key, expected) in [
|
|
("forward_unknown_method_audited", 200),
|
|
("forward_response_frame_hard_denied", 403),
|
|
] {
|
|
let expected_fragment = format!(r#""{key}": {expected}"#);
|
|
assert!(
|
|
guard.create_output.contains(&expected_fragment),
|
|
"expected {key}={expected}, got:\n{}",
|
|
guard.create_output
|
|
);
|
|
}
|
|
}
|