@vm0/runner 3.12.1 → 3.12.2
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/index.js +18 -511
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -48,7 +48,9 @@ var runnerPaths = {
|
|
|
48
48
|
/** Check if a directory name is a VM workspace */
|
|
49
49
|
isVmWorkspace: (dirname) => dirname.startsWith(VM_WORKSPACE_PREFIX),
|
|
50
50
|
/** Extract vmId from workspace directory name */
|
|
51
|
-
extractVmId: (dirname) => createVmId(dirname.replace(VM_WORKSPACE_PREFIX, ""))
|
|
51
|
+
extractVmId: (dirname) => createVmId(dirname.replace(VM_WORKSPACE_PREFIX, "")),
|
|
52
|
+
/** VM registry file for proxy IP → run mapping */
|
|
53
|
+
vmRegistry: (baseDir) => path.join(baseDir, "vm-registry.json")
|
|
52
54
|
};
|
|
53
55
|
var vmPaths = {
|
|
54
56
|
/** Firecracker config file (used with --config-file) */
|
|
@@ -73,8 +75,6 @@ var snapshotOutputPaths = {
|
|
|
73
75
|
var tempPaths = {
|
|
74
76
|
/** Default proxy CA directory */
|
|
75
77
|
proxyDir: `${VM0_TMP_PREFIX}-proxy`,
|
|
76
|
-
/** VM registry for proxy */
|
|
77
|
-
vmRegistry: `${VM0_TMP_PREFIX}-vm-registry.json`,
|
|
78
78
|
/** Network log file for a run */
|
|
79
79
|
networkLog: (runId) => `${VM0_TMP_PREFIX}-network-${runId}.jsonl`
|
|
80
80
|
};
|
|
@@ -9195,11 +9195,10 @@ var ENV_LOADER_PATH = "/usr/local/bin/vm0-agent/env-loader.mjs";
|
|
|
9195
9195
|
// src/lib/proxy/vm-registry.ts
|
|
9196
9196
|
import fs6 from "fs";
|
|
9197
9197
|
var logger5 = createLogger("VMRegistry");
|
|
9198
|
-
var DEFAULT_REGISTRY_PATH = tempPaths.vmRegistry;
|
|
9199
9198
|
var VMRegistry = class {
|
|
9200
9199
|
registryPath;
|
|
9201
9200
|
data;
|
|
9202
|
-
constructor(registryPath
|
|
9201
|
+
constructor(registryPath) {
|
|
9203
9202
|
this.registryPath = registryPath;
|
|
9204
9203
|
this.data = this.load();
|
|
9205
9204
|
}
|
|
@@ -9286,7 +9285,9 @@ var VMRegistry = class {
|
|
|
9286
9285
|
var globalRegistry = null;
|
|
9287
9286
|
function getVMRegistry() {
|
|
9288
9287
|
if (!globalRegistry) {
|
|
9289
|
-
|
|
9288
|
+
throw new Error(
|
|
9289
|
+
"VMRegistry not initialized. Call initVMRegistry(registryPath) first."
|
|
9290
|
+
);
|
|
9290
9291
|
}
|
|
9291
9292
|
return globalRegistry;
|
|
9292
9293
|
}
|
|
@@ -9299,502 +9300,16 @@ function initVMRegistry(registryPath) {
|
|
|
9299
9300
|
import { spawn as spawn2 } from "child_process";
|
|
9300
9301
|
import fs7 from "fs";
|
|
9301
9302
|
import path5 from "path";
|
|
9302
|
-
|
|
9303
|
-
// src/lib/proxy/mitm-addon-script.ts
|
|
9304
|
-
var RUNNER_MITM_ADDON_SCRIPT = `#!/usr/bin/env python3
|
|
9305
|
-
"""
|
|
9306
|
-
mitmproxy addon for VM0 runner-level network security mode.
|
|
9307
|
-
|
|
9308
|
-
This addon runs on the runner HOST (not inside VMs) and:
|
|
9309
|
-
1. Intercepts all HTTPS requests from VMs
|
|
9310
|
-
2. Looks up the source VM's runId and firewall rules from the VM registry
|
|
9311
|
-
3. Evaluates firewall rules (first-match-wins) to ALLOW or DENY
|
|
9312
|
-
4. For MITM mode: Rewrites requests to go through VM0 Proxy endpoint
|
|
9313
|
-
5. For SNI-only mode: Passes through or blocks without decryption
|
|
9314
|
-
6. Logs network activity per-run to JSONL files
|
|
9315
|
-
"""
|
|
9316
|
-
import os
|
|
9317
|
-
import json
|
|
9318
|
-
import time
|
|
9319
|
-
import urllib.parse
|
|
9320
|
-
import ipaddress
|
|
9321
|
-
import socket
|
|
9322
|
-
from mitmproxy import http, ctx, tls
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
# VM0 Proxy configuration from environment
|
|
9326
|
-
API_URL = os.environ.get("VM0_API_URL", "https://www.vm0.ai")
|
|
9327
|
-
REGISTRY_PATH = os.environ.get("VM0_REGISTRY_PATH", "/tmp/vm0-vm-registry.json")
|
|
9328
|
-
VERCEL_BYPASS = os.environ.get("VERCEL_AUTOMATION_BYPASS_SECRET", "")
|
|
9329
|
-
|
|
9330
|
-
# Construct proxy URL
|
|
9331
|
-
PROXY_URL = f"{API_URL}/api/webhooks/agent/proxy"
|
|
9332
|
-
|
|
9333
|
-
# Cache for VM registry (reloaded periodically)
|
|
9334
|
-
_registry_cache = {}
|
|
9335
|
-
_registry_cache_time = 0
|
|
9336
|
-
REGISTRY_CACHE_TTL = 2 # seconds
|
|
9337
|
-
|
|
9338
|
-
# Track request start times for latency calculation
|
|
9339
|
-
request_start_times = {}
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
def load_registry() -> dict:
|
|
9343
|
-
"""Load the VM registry from file, with caching."""
|
|
9344
|
-
global _registry_cache, _registry_cache_time
|
|
9345
|
-
|
|
9346
|
-
now = time.time()
|
|
9347
|
-
if now - _registry_cache_time < REGISTRY_CACHE_TTL:
|
|
9348
|
-
return _registry_cache
|
|
9349
|
-
|
|
9350
|
-
try:
|
|
9351
|
-
if os.path.exists(REGISTRY_PATH):
|
|
9352
|
-
with open(REGISTRY_PATH, "r") as f:
|
|
9353
|
-
data = json.load(f)
|
|
9354
|
-
_registry_cache = data.get("vms", {})
|
|
9355
|
-
_registry_cache_time = now
|
|
9356
|
-
return _registry_cache
|
|
9357
|
-
except Exception as e:
|
|
9358
|
-
ctx.log.warn(f"Failed to load VM registry: {e}")
|
|
9359
|
-
|
|
9360
|
-
return _registry_cache
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
def get_vm_info(client_ip: str) -> dict | None:
|
|
9364
|
-
"""Look up VM info by client IP address."""
|
|
9365
|
-
registry = load_registry()
|
|
9366
|
-
return registry.get(client_ip)
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
def get_network_log_path(run_id: str) -> str:
|
|
9370
|
-
"""Get the network log file path for a run."""
|
|
9371
|
-
return f"/tmp/vm0-network-{run_id}.jsonl"
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
def log_network_entry(run_id: str, entry: dict) -> None:
|
|
9375
|
-
"""Write a network log entry to the per-run JSONL file."""
|
|
9376
|
-
if not run_id:
|
|
9377
|
-
return
|
|
9378
|
-
|
|
9379
|
-
log_path = get_network_log_path(run_id)
|
|
9380
|
-
try:
|
|
9381
|
-
fd = os.open(log_path, os.O_CREAT | os.O_APPEND | os.O_WRONLY, 0o644)
|
|
9382
|
-
try:
|
|
9383
|
-
os.write(fd, (json.dumps(entry) + "\\n").encode())
|
|
9384
|
-
finally:
|
|
9385
|
-
os.close(fd)
|
|
9386
|
-
except Exception as e:
|
|
9387
|
-
ctx.log.warn(f"Failed to write network log: {e}")
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
def get_original_url(flow: http.HTTPFlow) -> str:
|
|
9391
|
-
"""Reconstruct the original target URL from the request."""
|
|
9392
|
-
scheme = "https" if flow.request.port == 443 else "http"
|
|
9393
|
-
host = flow.request.pretty_host
|
|
9394
|
-
port = flow.request.port
|
|
9395
|
-
|
|
9396
|
-
if (scheme == "https" and port != 443) or (scheme == "http" and port != 80):
|
|
9397
|
-
host_with_port = f"{host}:{port}"
|
|
9398
|
-
else:
|
|
9399
|
-
host_with_port = host
|
|
9400
|
-
|
|
9401
|
-
path = flow.request.path
|
|
9402
|
-
return f"{scheme}://{host_with_port}{path}"
|
|
9403
|
-
|
|
9404
|
-
|
|
9405
|
-
# ============================================================================
|
|
9406
|
-
# Firewall Rule Matching
|
|
9407
|
-
# ============================================================================
|
|
9408
|
-
|
|
9409
|
-
def match_domain(pattern: str, hostname: str) -> bool:
|
|
9410
|
-
"""
|
|
9411
|
-
Match hostname against domain pattern.
|
|
9412
|
-
Supports exact match and wildcard prefix (*.example.com).
|
|
9413
|
-
"""
|
|
9414
|
-
if not pattern or not hostname:
|
|
9415
|
-
return False
|
|
9416
|
-
|
|
9417
|
-
pattern = pattern.lower()
|
|
9418
|
-
hostname = hostname.lower()
|
|
9419
|
-
|
|
9420
|
-
if pattern.startswith("*."):
|
|
9421
|
-
# Wildcard: *.example.com matches sub.example.com, www.example.com
|
|
9422
|
-
# Also matches example.com itself (without subdomain)
|
|
9423
|
-
suffix = pattern[1:] # .example.com
|
|
9424
|
-
base = pattern[2:] # example.com
|
|
9425
|
-
return hostname.endswith(suffix) or hostname == base
|
|
9426
|
-
|
|
9427
|
-
return hostname == pattern
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
def match_ip(cidr: str, ip_str: str) -> bool:
|
|
9431
|
-
"""
|
|
9432
|
-
Match IP address against CIDR range.
|
|
9433
|
-
Supports single IPs (1.2.3.4) and ranges (10.0.0.0/8).
|
|
9434
|
-
"""
|
|
9435
|
-
if not cidr or not ip_str:
|
|
9436
|
-
return False
|
|
9437
|
-
|
|
9438
|
-
try:
|
|
9439
|
-
# Parse CIDR (automatically handles single IPs as /32)
|
|
9440
|
-
if "/" not in cidr:
|
|
9441
|
-
cidr = f"{cidr}/32"
|
|
9442
|
-
network = ipaddress.ip_network(cidr, strict=False)
|
|
9443
|
-
ip = ipaddress.ip_address(ip_str)
|
|
9444
|
-
return ip in network
|
|
9445
|
-
except ValueError:
|
|
9446
|
-
return False
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
def resolve_hostname_to_ip(hostname: str) -> str | None:
|
|
9450
|
-
"""Resolve hostname to IP address for IP-based rule matching."""
|
|
9451
|
-
try:
|
|
9452
|
-
return socket.gethostbyname(hostname)
|
|
9453
|
-
except socket.gaierror:
|
|
9454
|
-
return None
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
def evaluate_rules(rules: list, hostname: str, ip_str: str = None) -> tuple[str, str | None]:
|
|
9458
|
-
"""
|
|
9459
|
-
Evaluate firewall rules against hostname/IP.
|
|
9460
|
-
Returns (action, matched_rule_description).
|
|
9461
|
-
|
|
9462
|
-
Rule evaluation is first-match-wins (top to bottom).
|
|
9463
|
-
|
|
9464
|
-
Rule formats:
|
|
9465
|
-
- Domain/IP rule: { domain: "*.example.com", action: "ALLOW" }
|
|
9466
|
-
- Terminal rule: { final: "DENY" }
|
|
9467
|
-
"""
|
|
9468
|
-
if not rules:
|
|
9469
|
-
return ("ALLOW", None) # No rules = allow all
|
|
9470
|
-
|
|
9471
|
-
for rule in rules:
|
|
9472
|
-
# Final/terminal rule - value is the action
|
|
9473
|
-
final_action = rule.get("final")
|
|
9474
|
-
if final_action:
|
|
9475
|
-
return (final_action, "final")
|
|
9476
|
-
|
|
9477
|
-
# Domain rule
|
|
9478
|
-
domain = rule.get("domain")
|
|
9479
|
-
if domain and match_domain(domain, hostname):
|
|
9480
|
-
return (rule.get("action", "DENY"), f"domain:{domain}")
|
|
9481
|
-
|
|
9482
|
-
# IP rule
|
|
9483
|
-
ip_pattern = rule.get("ip")
|
|
9484
|
-
if ip_pattern:
|
|
9485
|
-
target_ip = ip_str
|
|
9486
|
-
if not target_ip:
|
|
9487
|
-
target_ip = resolve_hostname_to_ip(hostname)
|
|
9488
|
-
if target_ip and match_ip(ip_pattern, target_ip):
|
|
9489
|
-
return (rule.get("action", "DENY"), f"ip:{ip_pattern}")
|
|
9490
|
-
|
|
9491
|
-
# No rule matched - default deny (zero-trust)
|
|
9492
|
-
return ("DENY", "default")
|
|
9493
|
-
|
|
9494
|
-
|
|
9495
|
-
# ============================================================================
|
|
9496
|
-
# TLS ClientHello Handler (SNI-only mode)
|
|
9497
|
-
# ============================================================================
|
|
9498
|
-
|
|
9499
|
-
def tls_clienthello(data: tls.ClientHelloData) -> None:
|
|
9500
|
-
"""
|
|
9501
|
-
Handle TLS ClientHello for SNI-based filtering.
|
|
9502
|
-
This is called BEFORE TLS decryption, allowing SNI-only filtering.
|
|
9503
|
-
"""
|
|
9504
|
-
client_ip = data.context.client.peername[0] if data.context.client.peername else None
|
|
9505
|
-
if not client_ip:
|
|
9506
|
-
return
|
|
9507
|
-
|
|
9508
|
-
vm_info = get_vm_info(client_ip)
|
|
9509
|
-
if not vm_info:
|
|
9510
|
-
# Not a registered VM - pass through without MITM interception
|
|
9511
|
-
# This is critical for CIDR-based rules where all VM traffic is redirected
|
|
9512
|
-
data.ignore_connection = True
|
|
9513
|
-
return
|
|
9514
|
-
|
|
9515
|
-
# If MITM is enabled, let the normal flow handle it
|
|
9516
|
-
if vm_info.get("mitmEnabled", False):
|
|
9517
|
-
return
|
|
9518
|
-
|
|
9519
|
-
# SNI-only mode: check rules based on SNI
|
|
9520
|
-
sni = data.context.client.sni
|
|
9521
|
-
run_id = vm_info.get("runId", "")
|
|
9522
|
-
rules = vm_info.get("firewallRules", [])
|
|
9523
|
-
|
|
9524
|
-
# Auto-allow VM0 API requests - the agent MUST be able to communicate with VM0
|
|
9525
|
-
if API_URL and sni:
|
|
9526
|
-
parsed_api = urllib.parse.urlparse(API_URL)
|
|
9527
|
-
api_hostname = parsed_api.hostname.lower() if parsed_api.hostname else ""
|
|
9528
|
-
sni_lower = sni.lower()
|
|
9529
|
-
if api_hostname and (sni_lower == api_hostname or sni_lower.endswith(f".{api_hostname}")):
|
|
9530
|
-
ctx.log.info(f"[{run_id}] SNI-only auto-allow VM0 API: {sni}")
|
|
9531
|
-
log_network_entry(run_id, {
|
|
9532
|
-
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()),
|
|
9533
|
-
"mode": "sni",
|
|
9534
|
-
"action": "ALLOW",
|
|
9535
|
-
"host": sni,
|
|
9536
|
-
"port": 443,
|
|
9537
|
-
"rule_matched": "vm0-api",
|
|
9538
|
-
})
|
|
9539
|
-
data.ignore_connection = True # Pass through without MITM
|
|
9540
|
-
return
|
|
9541
|
-
|
|
9542
|
-
if not sni:
|
|
9543
|
-
# No SNI, can't determine target - block for security
|
|
9544
|
-
ctx.log.warn(f"[{run_id}] SNI-only: No SNI in ClientHello, blocking")
|
|
9545
|
-
log_network_entry(run_id, {
|
|
9546
|
-
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()),
|
|
9547
|
-
"mode": "sni",
|
|
9548
|
-
"action": "DENY",
|
|
9549
|
-
"host": "",
|
|
9550
|
-
"port": 443,
|
|
9551
|
-
"rule_matched": "no-sni",
|
|
9552
|
-
})
|
|
9553
|
-
# Don't set ignore_connection - mitmproxy will attempt MITM handshake
|
|
9554
|
-
# Since VM doesn't have CA cert (SNI-only mode), TLS will fail immediately
|
|
9555
|
-
return
|
|
9556
|
-
|
|
9557
|
-
# Evaluate rules
|
|
9558
|
-
action, matched_rule = evaluate_rules(rules, sni)
|
|
9559
|
-
|
|
9560
|
-
# Log the connection
|
|
9561
|
-
log_network_entry(run_id, {
|
|
9562
|
-
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()),
|
|
9563
|
-
"mode": "sni",
|
|
9564
|
-
"action": action,
|
|
9565
|
-
"host": sni,
|
|
9566
|
-
"port": 443,
|
|
9567
|
-
"rule_matched": matched_rule,
|
|
9568
|
-
})
|
|
9569
|
-
|
|
9570
|
-
if action == "ALLOW":
|
|
9571
|
-
# Pass through without MITM - mitmproxy will relay without decryption
|
|
9572
|
-
ctx.log.info(f"[{run_id}] SNI-only ALLOW: {sni} (rule: {matched_rule})")
|
|
9573
|
-
data.ignore_connection = True
|
|
9574
|
-
else:
|
|
9575
|
-
# Block the connection by NOT setting ignore_connection
|
|
9576
|
-
# mitmproxy will attempt MITM handshake, but since VM doesn't have
|
|
9577
|
-
# our CA certificate installed (SNI-only mode), the TLS handshake
|
|
9578
|
-
# will fail immediately with a certificate error.
|
|
9579
|
-
ctx.log.warn(f"[{run_id}] SNI-only DENY: {sni} (rule: {matched_rule})")
|
|
9580
|
-
# Client will see: SSL certificate problem / certificate verify failed
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
# ============================================================================
|
|
9584
|
-
# HTTP Request Handler (MITM mode)
|
|
9585
|
-
# ============================================================================
|
|
9586
|
-
|
|
9587
|
-
def request(flow: http.HTTPFlow) -> None:
|
|
9588
|
-
"""
|
|
9589
|
-
Intercept request and apply firewall rules.
|
|
9590
|
-
For MITM mode, rewrites allowed requests to VM0 Proxy.
|
|
9591
|
-
"""
|
|
9592
|
-
# Track request start time
|
|
9593
|
-
request_start_times[flow.id] = time.time()
|
|
9594
|
-
|
|
9595
|
-
# Get client IP (source VM)
|
|
9596
|
-
client_ip = flow.client_conn.peername[0] if flow.client_conn.peername else None
|
|
9597
|
-
|
|
9598
|
-
if not client_ip:
|
|
9599
|
-
ctx.log.warn("No client IP available, passing through")
|
|
9600
|
-
return
|
|
9601
|
-
|
|
9602
|
-
# Look up VM info from registry
|
|
9603
|
-
vm_info = get_vm_info(client_ip)
|
|
9604
|
-
|
|
9605
|
-
if not vm_info:
|
|
9606
|
-
# Not a registered VM, pass through without proxying
|
|
9607
|
-
ctx.log.info(f"No VM registration for {client_ip}, passing through")
|
|
9608
|
-
return
|
|
9609
|
-
|
|
9610
|
-
run_id = vm_info.get("runId", "")
|
|
9611
|
-
sandbox_token = vm_info.get("sandboxToken", "")
|
|
9612
|
-
mitm_enabled = vm_info.get("mitmEnabled", False)
|
|
9613
|
-
rules = vm_info.get("firewallRules", [])
|
|
9614
|
-
|
|
9615
|
-
# Store info for response handler
|
|
9616
|
-
flow.metadata["vm_run_id"] = run_id
|
|
9617
|
-
flow.metadata["vm_client_ip"] = client_ip
|
|
9618
|
-
flow.metadata["vm_mitm_enabled"] = mitm_enabled
|
|
9619
|
-
|
|
9620
|
-
# Get target hostname
|
|
9621
|
-
hostname = flow.request.pretty_host.lower()
|
|
9622
|
-
|
|
9623
|
-
# Auto-allow VM0 API requests - the agent MUST be able to communicate with VM0
|
|
9624
|
-
# This is checked before user firewall rules to ensure agent functionality
|
|
9625
|
-
if API_URL:
|
|
9626
|
-
parsed_api = urllib.parse.urlparse(API_URL)
|
|
9627
|
-
api_hostname = parsed_api.hostname.lower() if parsed_api.hostname else ""
|
|
9628
|
-
if api_hostname and (hostname == api_hostname or hostname.endswith(f".{api_hostname}")):
|
|
9629
|
-
ctx.log.info(f"[{run_id}] Auto-allow VM0 API: {hostname}")
|
|
9630
|
-
flow.metadata["firewall_action"] = "ALLOW"
|
|
9631
|
-
flow.metadata["firewall_rule"] = "vm0-api"
|
|
9632
|
-
# Continue to skip rewrite check below
|
|
9633
|
-
flow.metadata["original_url"] = get_original_url(flow)
|
|
9634
|
-
flow.metadata["skip_rewrite"] = True
|
|
9635
|
-
return
|
|
9636
|
-
|
|
9637
|
-
# Evaluate firewall rules
|
|
9638
|
-
action, matched_rule = evaluate_rules(rules, hostname)
|
|
9639
|
-
flow.metadata["firewall_action"] = action
|
|
9640
|
-
flow.metadata["firewall_rule"] = matched_rule
|
|
9641
|
-
|
|
9642
|
-
if action == "DENY":
|
|
9643
|
-
ctx.log.warn(f"[{run_id}] Firewall DENY: {hostname} (rule: {matched_rule})")
|
|
9644
|
-
# Kill the flow and return error response
|
|
9645
|
-
flow.response = http.Response.make(
|
|
9646
|
-
403,
|
|
9647
|
-
b"Blocked by firewall",
|
|
9648
|
-
{"Content-Type": "text/plain"}
|
|
9649
|
-
)
|
|
9650
|
-
return
|
|
9651
|
-
|
|
9652
|
-
# Request is ALLOWED - proceed with processing
|
|
9653
|
-
|
|
9654
|
-
# Skip if no API URL configured
|
|
9655
|
-
if not API_URL:
|
|
9656
|
-
ctx.log.warn("VM0_API_URL not set, passing through")
|
|
9657
|
-
return
|
|
9658
|
-
|
|
9659
|
-
# Skip rewriting requests already going to VM0 (avoid loops)
|
|
9660
|
-
if API_URL in flow.request.pretty_url:
|
|
9661
|
-
flow.metadata["original_url"] = flow.request.pretty_url
|
|
9662
|
-
flow.metadata["skip_rewrite"] = True
|
|
9663
|
-
return
|
|
9664
|
-
|
|
9665
|
-
# Skip rewriting requests to trusted storage domains (S3, etc.)
|
|
9666
|
-
# S3 presigned URLs have signatures that break when proxied
|
|
9667
|
-
TRUSTED_DOMAINS = [
|
|
9668
|
-
".s3.amazonaws.com",
|
|
9669
|
-
".s3-", # Regional S3 endpoints like s3-us-west-2.amazonaws.com
|
|
9670
|
-
"s3.amazonaws.com",
|
|
9671
|
-
".r2.cloudflarestorage.com",
|
|
9672
|
-
".storage.googleapis.com",
|
|
9673
|
-
]
|
|
9674
|
-
for domain in TRUSTED_DOMAINS:
|
|
9675
|
-
if domain in hostname or hostname.endswith(domain.lstrip(".")):
|
|
9676
|
-
ctx.log.info(f"[{run_id}] Skipping trusted storage domain: {hostname}")
|
|
9677
|
-
flow.metadata["original_url"] = get_original_url(flow)
|
|
9678
|
-
flow.metadata["skip_rewrite"] = True
|
|
9679
|
-
return
|
|
9680
|
-
|
|
9681
|
-
# Get original target URL
|
|
9682
|
-
original_url = get_original_url(flow)
|
|
9683
|
-
flow.metadata["original_url"] = original_url
|
|
9684
|
-
|
|
9685
|
-
# If MITM is not enabled, just allow the request through without rewriting
|
|
9686
|
-
if not mitm_enabled:
|
|
9687
|
-
ctx.log.info(f"[{run_id}] Firewall ALLOW (no MITM): {hostname}")
|
|
9688
|
-
return
|
|
9689
|
-
|
|
9690
|
-
# MITM mode: rewrite to VM0 Proxy
|
|
9691
|
-
ctx.log.info(f"[{run_id}] Proxying via MITM: {original_url}")
|
|
9692
|
-
|
|
9693
|
-
# Parse proxy URL
|
|
9694
|
-
parsed = urllib.parse.urlparse(PROXY_URL)
|
|
9695
|
-
|
|
9696
|
-
# Build query params
|
|
9697
|
-
query_params = {"url": original_url}
|
|
9698
|
-
if run_id:
|
|
9699
|
-
query_params["runId"] = run_id
|
|
9700
|
-
query_string = urllib.parse.urlencode(query_params)
|
|
9701
|
-
|
|
9702
|
-
# Rewrite request to proxy
|
|
9703
|
-
flow.request.host = parsed.hostname
|
|
9704
|
-
flow.request.port = 443 if parsed.scheme == "https" else 80
|
|
9705
|
-
flow.request.scheme = parsed.scheme
|
|
9706
|
-
flow.request.path = f"{parsed.path}?{query_string}"
|
|
9707
|
-
|
|
9708
|
-
# Save original Authorization header before overwriting
|
|
9709
|
-
if "Authorization" in flow.request.headers:
|
|
9710
|
-
flow.request.headers["x-vm0-original-authorization"] = flow.request.headers["Authorization"]
|
|
9711
|
-
|
|
9712
|
-
# Add sandbox authentication token
|
|
9713
|
-
if sandbox_token:
|
|
9714
|
-
flow.request.headers["Authorization"] = f"Bearer {sandbox_token}"
|
|
9715
|
-
|
|
9716
|
-
# Add Vercel bypass header if configured
|
|
9717
|
-
if VERCEL_BYPASS:
|
|
9718
|
-
flow.request.headers["x-vercel-protection-bypass"] = VERCEL_BYPASS
|
|
9719
|
-
|
|
9720
|
-
|
|
9721
|
-
def response(flow: http.HTTPFlow) -> None:
|
|
9722
|
-
"""
|
|
9723
|
-
Handle response and log network activity.
|
|
9724
|
-
"""
|
|
9725
|
-
# Calculate latency
|
|
9726
|
-
start_time = request_start_times.pop(flow.id, None)
|
|
9727
|
-
latency_ms = int((time.time() - start_time) * 1000) if start_time else 0
|
|
9728
|
-
|
|
9729
|
-
# Get stored info
|
|
9730
|
-
run_id = flow.metadata.get("vm_run_id", "")
|
|
9731
|
-
original_url = flow.metadata.get("original_url", flow.request.pretty_url)
|
|
9732
|
-
mitm_enabled = flow.metadata.get("vm_mitm_enabled", False)
|
|
9733
|
-
firewall_action = flow.metadata.get("firewall_action", "ALLOW")
|
|
9734
|
-
firewall_rule = flow.metadata.get("firewall_rule")
|
|
9735
|
-
|
|
9736
|
-
# Calculate sizes
|
|
9737
|
-
request_size = len(flow.request.content) if flow.request.content else 0
|
|
9738
|
-
response_size = len(flow.response.content) if flow.response and flow.response.content else 0
|
|
9739
|
-
status_code = flow.response.status_code if flow.response else 0
|
|
9740
|
-
|
|
9741
|
-
# Parse URL for host
|
|
9742
|
-
try:
|
|
9743
|
-
parsed_url = urllib.parse.urlparse(original_url)
|
|
9744
|
-
host = parsed_url.hostname or flow.request.pretty_host
|
|
9745
|
-
port = parsed_url.port or (443 if parsed_url.scheme == "https" else 80)
|
|
9746
|
-
except:
|
|
9747
|
-
host = flow.request.pretty_host
|
|
9748
|
-
port = flow.request.port
|
|
9749
|
-
|
|
9750
|
-
# Log network entry for this run
|
|
9751
|
-
if run_id:
|
|
9752
|
-
log_entry = {
|
|
9753
|
-
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()),
|
|
9754
|
-
"mode": "mitm" if mitm_enabled else "sni",
|
|
9755
|
-
"action": firewall_action,
|
|
9756
|
-
"host": host,
|
|
9757
|
-
"port": port,
|
|
9758
|
-
"rule_matched": firewall_rule,
|
|
9759
|
-
}
|
|
9760
|
-
|
|
9761
|
-
# Add HTTP details only in MITM mode
|
|
9762
|
-
if mitm_enabled:
|
|
9763
|
-
log_entry.update({
|
|
9764
|
-
"method": flow.request.method,
|
|
9765
|
-
"path": flow.request.path.split("?")[0], # Path without query
|
|
9766
|
-
"url": original_url,
|
|
9767
|
-
"status": status_code,
|
|
9768
|
-
"latency_ms": latency_ms,
|
|
9769
|
-
"request_size": request_size,
|
|
9770
|
-
"response_size": response_size,
|
|
9771
|
-
})
|
|
9772
|
-
|
|
9773
|
-
log_network_entry(run_id, log_entry)
|
|
9774
|
-
|
|
9775
|
-
# Log errors to mitmproxy console
|
|
9776
|
-
if flow.response and flow.response.status_code >= 400:
|
|
9777
|
-
ctx.log.warn(
|
|
9778
|
-
f"[{run_id}] Response {flow.response.status_code}: {original_url}"
|
|
9779
|
-
)
|
|
9780
|
-
|
|
9781
|
-
|
|
9782
|
-
# mitmproxy addon registration
|
|
9783
|
-
addons = [tls_clienthello, request, response]
|
|
9784
|
-
`;
|
|
9785
|
-
|
|
9786
|
-
// src/lib/proxy/proxy-manager.ts
|
|
9787
9303
|
var logger6 = createLogger("ProxyManager");
|
|
9788
9304
|
var DEFAULT_PROXY_OPTIONS = {
|
|
9789
|
-
port: 8080
|
|
9790
|
-
registryPath: DEFAULT_REGISTRY_PATH
|
|
9305
|
+
port: 8080
|
|
9791
9306
|
};
|
|
9792
9307
|
var ProxyManager = class {
|
|
9793
9308
|
config;
|
|
9794
9309
|
process = null;
|
|
9795
9310
|
isRunning = false;
|
|
9796
9311
|
constructor(config) {
|
|
9797
|
-
const addonPath = path5.join(config.caDir, "
|
|
9312
|
+
const addonPath = path5.join(config.caDir, "mitm-addon.py");
|
|
9798
9313
|
this.config = {
|
|
9799
9314
|
...DEFAULT_PROXY_OPTIONS,
|
|
9800
9315
|
...config,
|
|
@@ -9817,19 +9332,6 @@ var ProxyManager = class {
|
|
|
9817
9332
|
});
|
|
9818
9333
|
});
|
|
9819
9334
|
}
|
|
9820
|
-
/**
|
|
9821
|
-
* Ensure the addon script exists at the configured path
|
|
9822
|
-
*/
|
|
9823
|
-
ensureAddonScript() {
|
|
9824
|
-
const addonDir = path5.dirname(this.config.addonPath);
|
|
9825
|
-
if (!fs7.existsSync(addonDir)) {
|
|
9826
|
-
fs7.mkdirSync(addonDir, { recursive: true });
|
|
9827
|
-
}
|
|
9828
|
-
fs7.writeFileSync(this.config.addonPath, RUNNER_MITM_ADDON_SCRIPT, {
|
|
9829
|
-
mode: 493
|
|
9830
|
-
});
|
|
9831
|
-
logger6.log(`Addon script written to ${this.config.addonPath}`);
|
|
9832
|
-
}
|
|
9833
9335
|
/**
|
|
9834
9336
|
* Validate proxy configuration
|
|
9835
9337
|
*/
|
|
@@ -9841,7 +9343,9 @@ var ProxyManager = class {
|
|
|
9841
9343
|
if (!fs7.existsSync(caCertPath)) {
|
|
9842
9344
|
throw new Error(`Proxy CA certificate not found: ${caCertPath}`);
|
|
9843
9345
|
}
|
|
9844
|
-
this.
|
|
9346
|
+
if (!fs7.existsSync(this.config.addonPath)) {
|
|
9347
|
+
throw new Error(`Addon script not found: ${this.config.addonPath}`);
|
|
9348
|
+
}
|
|
9845
9349
|
}
|
|
9846
9350
|
/**
|
|
9847
9351
|
* Start mitmproxy
|
|
@@ -10588,11 +10092,13 @@ async function setupEnvironment(options) {
|
|
|
10588
10092
|
process.exit(1);
|
|
10589
10093
|
}
|
|
10590
10094
|
logger12.log("Initializing network proxy...");
|
|
10591
|
-
|
|
10095
|
+
const registryPath = runnerPaths.vmRegistry(config.base_dir);
|
|
10096
|
+
initVMRegistry(registryPath);
|
|
10592
10097
|
const proxyManager = initProxyManager({
|
|
10593
10098
|
apiUrl: config.server.url,
|
|
10594
10099
|
port: config.proxy.port,
|
|
10595
|
-
caDir: config.proxy.ca_dir
|
|
10100
|
+
caDir: config.proxy.ca_dir,
|
|
10101
|
+
registryPath
|
|
10596
10102
|
});
|
|
10597
10103
|
let proxyEnabled = false;
|
|
10598
10104
|
try {
|
|
@@ -11502,6 +11008,7 @@ var benchmarkCommand = new Command4("benchmark").description(
|
|
|
11502
11008
|
}
|
|
11503
11009
|
process.exit(1);
|
|
11504
11010
|
}
|
|
11011
|
+
initVMRegistry(runnerPaths.vmRegistry(config.base_dir));
|
|
11505
11012
|
timer.log("Initializing pools...");
|
|
11506
11013
|
const snapshotConfig = config.firecracker.snapshot;
|
|
11507
11014
|
await initOverlayPool({
|
|
@@ -11747,7 +11254,7 @@ var snapshotCommand = new Command5("snapshot").description("Generate a Firecrack
|
|
|
11747
11254
|
);
|
|
11748
11255
|
|
|
11749
11256
|
// src/index.ts
|
|
11750
|
-
var version = true ? "3.12.
|
|
11257
|
+
var version = true ? "3.12.2" : "0.1.0";
|
|
11751
11258
|
program.name("vm0-runner").version(version).description("Self-hosted runner for VM0 agents");
|
|
11752
11259
|
program.addCommand(startCommand);
|
|
11753
11260
|
program.addCommand(doctorCommand);
|