Spaces:
Sleeping
Sleeping
Commit
·
b299500
1
Parent(s):
b851ab5
LANDRUN INTEGRATION - Kernel-level Linux sandbox with Landlock security
Browse files- app.py +331 -863
- landrun-main/.github/workflows/build.yml +31 -0
- landrun-main/.github/workflows/go-compatibility.yml +24 -0
- landrun-main/.gitignore +31 -0
- landrun-main/LICENSE +21 -0
- landrun-main/README.md +400 -0
- landrun-main/demo.gif +0 -0
- landrun-main/go.mod +16 -0
- landrun-main/go.sum +14 -0
- landrun-main/internal/elfdeps/elfdeps.go +230 -0
- landrun-main/internal/elfdeps/elfdeps_test.go +222 -0
- landrun-main/internal/elfdeps/testdata/liba.c +1 -0
- landrun-main/internal/elfdeps/testdata/libb.c +2 -0
- landrun-main/internal/elfdeps/testdata/main.c +2 -0
- landrun-main/internal/exec/runner.go +21 -0
- landrun-main/internal/log/log.go +64 -0
- landrun-main/internal/sandbox/sandbox.go +192 -0
- landrun-main/test.sh +314 -0
app.py
CHANGED
|
@@ -1,18 +1,20 @@
|
|
| 1 |
"""
|
| 2 |
-
FastAPI Universal Code Execution Sandbox
|
| 3 |
-
|
| 4 |
"""
|
| 5 |
|
| 6 |
from fastapi import FastAPI, Request
|
| 7 |
-
from fastapi.responses import HTMLResponse,
|
| 8 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
|
|
|
| 9 |
import os
|
| 10 |
-
import
|
| 11 |
-
import
|
| 12 |
|
| 13 |
app = FastAPI()
|
| 14 |
|
| 15 |
-
# Enable CORS
|
| 16 |
app.add_middleware(
|
| 17 |
CORSMiddleware,
|
| 18 |
allow_origins=["*"],
|
|
@@ -21,951 +23,417 @@ app.add_middleware(
|
|
| 21 |
allow_headers=["*"],
|
| 22 |
)
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
"
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
"
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
"
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
"
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
"extension": ".php",
|
| 50 |
-
"command": lambda file: ["php", file],
|
| 51 |
-
"runtime_check": "php"
|
| 52 |
-
},
|
| 53 |
-
"go": {
|
| 54 |
-
"extension": ".go",
|
| 55 |
-
"command": lambda file: ["go", "run", file],
|
| 56 |
-
"runtime_check": "go"
|
| 57 |
-
},
|
| 58 |
-
"rust": {
|
| 59 |
-
"extension": ".rs",
|
| 60 |
-
"compile": lambda file: ["rustc", file, "-o", "/tmp/rust_output"],
|
| 61 |
-
"command": lambda: ["/tmp/rust_output"],
|
| 62 |
-
"runtime_check": "rustc",
|
| 63 |
-
"needs_compile": True
|
| 64 |
-
},
|
| 65 |
-
"cpp": {
|
| 66 |
-
"extension": ".cpp",
|
| 67 |
-
"compile": lambda file: ["g++", file, "-o", "/tmp/cpp_output"],
|
| 68 |
-
"command": lambda: ["/tmp/cpp_output"],
|
| 69 |
-
"runtime_check": "g++",
|
| 70 |
-
"needs_compile": True
|
| 71 |
-
},
|
| 72 |
-
"c": {
|
| 73 |
-
"extension": ".c",
|
| 74 |
-
"compile": lambda file: ["gcc", file, "-o", "/tmp/c_output"],
|
| 75 |
-
"command": lambda: ["/tmp/c_output"],
|
| 76 |
-
"runtime_check": "gcc",
|
| 77 |
-
"needs_compile": True
|
| 78 |
-
},
|
| 79 |
-
"csharp": {
|
| 80 |
-
"extension": ".cs",
|
| 81 |
-
"compile": lambda file: ["mcs", file, "-out:/tmp/csharp_output.exe"],
|
| 82 |
-
"command": lambda: ["mono", "/tmp/csharp_output.exe"],
|
| 83 |
-
"runtime_check": "mcs",
|
| 84 |
-
"needs_compile": True
|
| 85 |
-
},
|
| 86 |
-
"swift": {
|
| 87 |
-
"extension": ".swift",
|
| 88 |
-
"command": lambda file: ["swift", file],
|
| 89 |
-
"runtime_check": "swift"
|
| 90 |
-
},
|
| 91 |
-
"kotlin": {
|
| 92 |
-
"extension": ".kt",
|
| 93 |
-
"compile": lambda file: ["kotlinc", file, "-include-runtime", "-d", "/tmp/kotlin_output.jar"],
|
| 94 |
-
"command": lambda: ["java", "-jar", "/tmp/kotlin_output.jar"],
|
| 95 |
-
"runtime_check": "kotlinc",
|
| 96 |
-
"needs_compile": True
|
| 97 |
-
},
|
| 98 |
-
"perl": {
|
| 99 |
-
"extension": ".pl",
|
| 100 |
-
"command": lambda file: ["perl", file],
|
| 101 |
-
"runtime_check": "perl"
|
| 102 |
-
},
|
| 103 |
-
"lua": {
|
| 104 |
-
"extension": ".lua",
|
| 105 |
-
"command": lambda file: ["lua", file],
|
| 106 |
-
"runtime_check": "lua"
|
| 107 |
-
},
|
| 108 |
-
"bash": {
|
| 109 |
-
"extension": ".sh",
|
| 110 |
-
"command": lambda file: ["bash", file],
|
| 111 |
-
"runtime_check": "bash"
|
| 112 |
-
},
|
| 113 |
-
"r": {
|
| 114 |
-
"extension": ".r",
|
| 115 |
-
"command": lambda file: ["Rscript", file],
|
| 116 |
-
"runtime_check": "Rscript"
|
| 117 |
-
},
|
| 118 |
-
"scala": {
|
| 119 |
-
"extension": ".scala",
|
| 120 |
-
"command": lambda file: ["scala", file],
|
| 121 |
-
"runtime_check": "scala"
|
| 122 |
}
|
| 123 |
-
}
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
@app.get("/health")
|
| 127 |
-
async def health():
|
| 128 |
-
"""Health check for pre-warming the Space"""
|
| 129 |
-
return {"status": "ok"}
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
@app.get("/capabilities")
|
| 133 |
-
async def capabilities():
|
| 134 |
-
"""Check which language runtimes are available"""
|
| 135 |
-
caps = {}
|
| 136 |
-
for lang, config in LANGUAGE_CONFIG.items():
|
| 137 |
-
runtime = config.get("runtime_check")
|
| 138 |
-
caps[lang] = shutil.which(runtime) is not None
|
| 139 |
-
return caps
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
@app.get("/languages")
|
| 143 |
-
async def list_languages():
|
| 144 |
-
"""List all supported languages with availability"""
|
| 145 |
-
result = []
|
| 146 |
-
for lang, config in LANGUAGE_CONFIG.items():
|
| 147 |
-
runtime = config.get("runtime_check")
|
| 148 |
-
available = shutil.which(runtime) is not None
|
| 149 |
-
result.append({
|
| 150 |
-
"language": lang,
|
| 151 |
-
"available": available,
|
| 152 |
-
"extension": config["extension"],
|
| 153 |
-
"needs_compile": config.get("needs_compile", False)
|
| 154 |
-
})
|
| 155 |
-
return {"languages": result}
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
async def stream_execution(language: str, code: str):
|
| 159 |
-
"""Execute code for any supported language and stream output"""
|
| 160 |
|
| 161 |
-
config =
|
| 162 |
if not config:
|
| 163 |
-
|
| 164 |
-
yield "done\n"
|
| 165 |
-
return
|
| 166 |
|
| 167 |
-
#
|
| 168 |
-
runtime = config.get("runtime_check")
|
| 169 |
-
if not shutil.which(runtime):
|
| 170 |
-
yield f"Error: {language} runtime not available\n"
|
| 171 |
-
yield "done\n"
|
| 172 |
-
return
|
| 173 |
-
|
| 174 |
-
# Determine filename
|
| 175 |
-
if language == "java":
|
| 176 |
-
code_path = "/tmp/Main.java"
|
| 177 |
-
else:
|
| 178 |
-
code_path = f"/tmp/code{config['extension']}"
|
| 179 |
-
|
| 180 |
-
# Write code to file
|
| 181 |
try:
|
| 182 |
-
with
|
| 183 |
f.write(code)
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
*compile_cmd,
|
| 196 |
-
stdout=asyncio.subprocess.PIPE,
|
| 197 |
-
stderr=asyncio.subprocess.STDOUT
|
| 198 |
-
)
|
| 199 |
-
|
| 200 |
-
# Stream compilation output
|
| 201 |
-
while True:
|
| 202 |
-
line = await compile_process.stdout.readline()
|
| 203 |
-
if not line:
|
| 204 |
-
break
|
| 205 |
-
yield line.decode('utf-8', errors='replace')
|
| 206 |
-
|
| 207 |
-
exit_code = await compile_process.wait()
|
| 208 |
-
|
| 209 |
-
if exit_code != 0:
|
| 210 |
-
yield f"Compilation failed with exit code {exit_code}\n"
|
| 211 |
-
yield "done\n"
|
| 212 |
-
return
|
| 213 |
-
|
| 214 |
-
# Execute
|
| 215 |
-
if config.get("needs_compile") and callable(config["command"]):
|
| 216 |
-
run_cmd = config["command"]()
|
| 217 |
-
else:
|
| 218 |
-
run_cmd = config["command"](code_path)
|
| 219 |
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
)
|
| 225 |
|
| 226 |
-
#
|
| 227 |
-
|
| 228 |
-
line = await run_process.stdout.readline()
|
| 229 |
-
if not line:
|
| 230 |
-
break
|
| 231 |
-
yield line.decode('utf-8', errors='replace')
|
| 232 |
|
| 233 |
-
|
| 234 |
-
|
|
|
|
|
|
|
| 235 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
except Exception as e:
|
| 237 |
-
|
| 238 |
-
yield "done\n"
|
| 239 |
-
|
| 240 |
finally:
|
| 241 |
-
#
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
elif language == "rust":
|
| 248 |
-
os.remove("/tmp/rust_output")
|
| 249 |
-
elif language == "cpp":
|
| 250 |
-
os.remove("/tmp/cpp_output")
|
| 251 |
-
elif language == "c":
|
| 252 |
-
os.remove("/tmp/c_output")
|
| 253 |
-
elif language == "csharp":
|
| 254 |
-
os.remove("/tmp/csharp_output.exe")
|
| 255 |
-
elif language == "kotlin":
|
| 256 |
-
os.remove("/tmp/kotlin_output.jar")
|
| 257 |
-
except Exception:
|
| 258 |
-
pass
|
| 259 |
-
|
| 260 |
-
# Schedule exit
|
| 261 |
-
asyncio.create_task(delayed_exit())
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
async def delayed_exit():
|
| 265 |
-
"""Exit the process after a short delay"""
|
| 266 |
-
await asyncio.sleep(0.5)
|
| 267 |
-
os._exit(0)
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
@app.post("/run")
|
| 271 |
-
async def run_code(request: Request):
|
| 272 |
-
"""Execute code in any supported language and stream output"""
|
| 273 |
-
body = await request.json()
|
| 274 |
-
language = body.get("language", "").lower()
|
| 275 |
-
code = body.get("code", "")
|
| 276 |
-
|
| 277 |
-
if not code:
|
| 278 |
-
return JSONResponse({"error": "NO_CODE"}, status_code=400)
|
| 279 |
-
|
| 280 |
-
if language not in LANGUAGE_CONFIG:
|
| 281 |
-
return JSONResponse(
|
| 282 |
-
{"error": f"UNSUPPORTED_LANGUAGE: {language}"},
|
| 283 |
-
status_code=400
|
| 284 |
-
)
|
| 285 |
-
|
| 286 |
-
# Check if runtime is available
|
| 287 |
-
config = LANGUAGE_CONFIG[language]
|
| 288 |
-
runtime = config.get("runtime_check")
|
| 289 |
-
if not shutil.which(runtime):
|
| 290 |
-
return JSONResponse(
|
| 291 |
-
{"error": f"{language.upper()}_NOT_AVAILABLE"},
|
| 292 |
-
status_code=503
|
| 293 |
-
)
|
| 294 |
-
|
| 295 |
-
return StreamingResponse(
|
| 296 |
-
stream_execution(language, code),
|
| 297 |
-
media_type="text/plain"
|
| 298 |
-
)
|
| 299 |
|
| 300 |
|
| 301 |
@app.get("/", response_class=HTMLResponse)
|
| 302 |
async def root():
|
| 303 |
-
"""Serve the
|
| 304 |
-
|
| 305 |
<!DOCTYPE html>
|
| 306 |
<html lang="en">
|
| 307 |
<head>
|
| 308 |
<meta charset="UTF-8">
|
| 309 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 310 |
-
<title
|
| 311 |
-
|
| 312 |
-
<!-- CDN Scripts -->
|
| 313 |
-
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js"></script>
|
| 314 |
-
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
| 315 |
-
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
| 316 |
-
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
| 317 |
-
|
| 318 |
<style>
|
| 319 |
-
* {
|
| 320 |
-
margin: 0;
|
| 321 |
-
padding: 0;
|
| 322 |
-
box-sizing: border-box;
|
| 323 |
-
}
|
| 324 |
-
|
| 325 |
body {
|
| 326 |
-
font-family: '
|
| 327 |
-
background: #
|
| 328 |
-
|
| 329 |
-
height: 100vh;
|
| 330 |
-
overflow: hidden;
|
| 331 |
-
display: flex;
|
| 332 |
-
flex-direction: column;
|
| 333 |
-
}
|
| 334 |
-
|
| 335 |
-
.app-container {
|
| 336 |
-
display: flex;
|
| 337 |
-
flex-direction: column;
|
| 338 |
-
height: 100vh;
|
| 339 |
padding: 20px;
|
| 340 |
-
gap: 15px;
|
| 341 |
}
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
|
|
|
|
|
|
| 347 |
}
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
align
|
| 353 |
-
flex-wrap: wrap;
|
| 354 |
-
flex-shrink: 0;
|
| 355 |
}
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
font-weight: bold;
|
| 360 |
}
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
padding: 10px 15px;
|
| 367 |
-
border-radius: 4px;
|
| 368 |
-
font-family: inherit;
|
| 369 |
-
font-size: 14px;
|
| 370 |
-
cursor: pointer;
|
| 371 |
-
transition: all 0.2s;
|
| 372 |
}
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
border-
|
|
|
|
| 376 |
}
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
}
|
| 382 |
-
|
| 383 |
button {
|
| 384 |
-
background: #
|
| 385 |
color: white;
|
| 386 |
border: none;
|
|
|
|
|
|
|
| 387 |
font-weight: bold;
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
transform: translateY(-1px);
|
| 394 |
-
}
|
| 395 |
-
|
| 396 |
-
button:active:not(:disabled) {
|
| 397 |
-
transform: translateY(0);
|
| 398 |
}
|
| 399 |
-
|
| 400 |
button:disabled {
|
| 401 |
-
background: #
|
| 402 |
cursor: not-allowed;
|
| 403 |
-
|
| 404 |
}
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
flex: 1;
|
| 408 |
-
display: grid;
|
| 409 |
-
grid-template-rows: minmax(250px, 1fr) minmax(250px, 1fr);
|
| 410 |
-
gap: 15px;
|
| 411 |
-
min-height: 0;
|
| 412 |
-
overflow: hidden;
|
| 413 |
-
}
|
| 414 |
-
|
| 415 |
-
.editor-panel {
|
| 416 |
-
display: flex;
|
| 417 |
-
flex-direction: column;
|
| 418 |
-
min-height: 0;
|
| 419 |
-
}
|
| 420 |
-
|
| 421 |
-
.panel-label {
|
| 422 |
-
color: #9cdcfe;
|
| 423 |
-
font-weight: bold;
|
| 424 |
-
margin-bottom: 8px;
|
| 425 |
-
font-size: 13px;
|
| 426 |
-
text-transform: uppercase;
|
| 427 |
-
letter-spacing: 1px;
|
| 428 |
-
}
|
| 429 |
-
|
| 430 |
-
textarea {
|
| 431 |
-
flex: 1;
|
| 432 |
-
background: #252526;
|
| 433 |
color: #d4d4d4;
|
| 434 |
-
|
| 435 |
-
border-radius:
|
| 436 |
-
|
| 437 |
-
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
|
| 438 |
font-size: 14px;
|
| 439 |
-
line-height: 1.6;
|
| 440 |
-
resize: none;
|
| 441 |
-
outline: none;
|
| 442 |
-
min-height: 0;
|
| 443 |
-
}
|
| 444 |
-
|
| 445 |
-
textarea:focus {
|
| 446 |
-
border-color: #007acc;
|
| 447 |
-
}
|
| 448 |
-
|
| 449 |
-
.output-panel {
|
| 450 |
-
display: flex;
|
| 451 |
-
flex-direction: column;
|
| 452 |
-
min-height: 0;
|
| 453 |
-
position: relative;
|
| 454 |
-
}
|
| 455 |
-
|
| 456 |
-
#output {
|
| 457 |
-
flex: 1;
|
| 458 |
-
background: #0c0c0c;
|
| 459 |
-
color: #00ff00;
|
| 460 |
-
border: 1px solid #00ff00;
|
| 461 |
-
border-radius: 4px;
|
| 462 |
-
padding: 15px;
|
| 463 |
-
overflow-y: auto;
|
| 464 |
-
font-size: 13px;
|
| 465 |
-
line-height: 1.6;
|
| 466 |
white-space: pre-wrap;
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
}
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
|
|
|
|
|
|
| 474 |
background: white;
|
| 475 |
-
border: 1px solid #3c3c3c;
|
| 476 |
-
border-radius: 4px;
|
| 477 |
-
overflow: auto;
|
| 478 |
-
min-height: 0;
|
| 479 |
-
}
|
| 480 |
-
|
| 481 |
-
#html-preview {
|
| 482 |
-
border: 1px solid #3c3c3c;
|
| 483 |
}
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
.badge {
|
| 490 |
-
display: inline-block;
|
| 491 |
-
padding: 3px 8px;
|
| 492 |
-
border-radius: 3px;
|
| 493 |
-
font-size: 11px;
|
| 494 |
-
margin-left: 5px;
|
| 495 |
font-weight: bold;
|
| 496 |
}
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
/* Scrollbar styling */
|
| 502 |
-
::-webkit-scrollbar {
|
| 503 |
-
width: 10px;
|
| 504 |
-
height: 10px;
|
| 505 |
}
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
|
|
|
| 509 |
}
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
|
|
|
| 514 |
}
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
background:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 518 |
}
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
grid-template-rows: 300px 1fr;
|
| 523 |
-
}
|
| 524 |
-
|
| 525 |
-
h1 {
|
| 526 |
-
font-size: 20px;
|
| 527 |
-
}
|
| 528 |
}
|
| 529 |
</style>
|
| 530 |
</head>
|
| 531 |
<body>
|
| 532 |
-
<div class="
|
| 533 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 534 |
|
| 535 |
-
<div class="
|
| 536 |
-
<
|
| 537 |
-
|
| 538 |
-
<
|
| 539 |
-
<option value="html">HTML</option>
|
| 540 |
-
<option value="react">React/JSX</option>
|
| 541 |
-
</optgroup>
|
| 542 |
-
<optgroup label="Scripting Languages">
|
| 543 |
<option value="python">Python</option>
|
| 544 |
-
<option value="
|
| 545 |
-
<option value="
|
| 546 |
-
<option value="
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
<
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
<
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
</select>
|
| 564 |
|
| 565 |
-
<
|
| 566 |
-
|
| 567 |
-
|
|
|
|
|
|
|
| 568 |
</div>
|
| 569 |
|
| 570 |
-
<div
|
| 571 |
-
<div class="
|
| 572 |
-
<
|
| 573 |
-
<
|
| 574 |
-
</div>
|
| 575 |
-
|
| 576 |
-
<div class="output-panel">
|
| 577 |
-
<div class="panel-label">◆ Output / Preview</div>
|
| 578 |
-
<pre id="output" style="display: none;"></pre>
|
| 579 |
-
<iframe id="html-preview" style="display: none;" sandbox="allow-scripts"></iframe>
|
| 580 |
-
<div id="react-root" style="display: none;"></div>
|
| 581 |
</div>
|
| 582 |
</div>
|
| 583 |
</div>
|
| 584 |
|
| 585 |
<script>
|
| 586 |
-
const editor = document.getElementById('editor');
|
| 587 |
-
const output = document.getElementById('output');
|
| 588 |
-
const htmlPreview = document.getElementById('html-preview');
|
| 589 |
-
const reactRoot = document.getElementById('react-root');
|
| 590 |
-
const languageSelect = document.getElementById('language');
|
| 591 |
-
const runBtn = document.getElementById('runBtn');
|
| 592 |
-
const status = document.getElementById('status');
|
| 593 |
-
|
| 594 |
-
let capabilities = {};
|
| 595 |
-
|
| 596 |
-
// Fetch capabilities on load
|
| 597 |
-
fetch('/capabilities')
|
| 598 |
-
.then(res => res.json())
|
| 599 |
-
.then(data => {
|
| 600 |
-
capabilities = data;
|
| 601 |
-
updateLanguageSelector();
|
| 602 |
-
});
|
| 603 |
-
|
| 604 |
-
function updateLanguageSelector() {
|
| 605 |
-
const options = languageSelect.querySelectorAll('option');
|
| 606 |
-
options.forEach(opt => {
|
| 607 |
-
const lang = opt.value;
|
| 608 |
-
if (lang === 'html' || lang === 'react') {
|
| 609 |
-
return; // Client-side always available
|
| 610 |
-
}
|
| 611 |
-
if (capabilities[lang] !== undefined) {
|
| 612 |
-
const badge = capabilities[lang] ?
|
| 613 |
-
'<span class="badge badge-available">✓</span>' :
|
| 614 |
-
'<span class="badge badge-unavailable">✗</span>';
|
| 615 |
-
opt.innerHTML = opt.innerHTML.split('<')[0] + ' ' + (capabilities[lang] ? '✓' : '✗');
|
| 616 |
-
}
|
| 617 |
-
});
|
| 618 |
-
}
|
| 619 |
-
|
| 620 |
-
async function checkAvailability() {
|
| 621 |
-
status.textContent = '🔍 Checking...';
|
| 622 |
-
status.className = 'info';
|
| 623 |
-
|
| 624 |
-
const response = await fetch('/languages');
|
| 625 |
-
const data = await response.json();
|
| 626 |
-
|
| 627 |
-
output.style.display = 'block';
|
| 628 |
-
htmlPreview.style.display = 'none';
|
| 629 |
-
reactRoot.style.display = 'none';
|
| 630 |
-
|
| 631 |
-
output.textContent = 'Available Languages:\\n\\n';
|
| 632 |
-
data.languages.forEach(lang => {
|
| 633 |
-
const badge = lang.available ? '✅' : '❌';
|
| 634 |
-
output.textContent += `${badge} ${lang.language.toUpperCase()} (${lang.extension})\\n`;
|
| 635 |
-
});
|
| 636 |
-
|
| 637 |
-
status.textContent = '✓ Check complete';
|
| 638 |
-
status.className = 'success';
|
| 639 |
-
}
|
| 640 |
-
|
| 641 |
const examples = {
|
| 642 |
-
|
| 643 |
-
print("Hello from
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
}
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
for (int i = 0; i < 5; i++) {
|
| 660 |
-
System.out.println("Step " + (i + 1));
|
| 661 |
-
}
|
| 662 |
-
System.out.println("Done!");
|
| 663 |
-
}
|
| 664 |
-
}`,
|
| 665 |
-
|
| 666 |
-
ruby: `# Ruby example
|
| 667 |
-
puts "Hello from Ruby!"
|
| 668 |
-
5.times do |i|
|
| 669 |
-
puts "Step #{i + 1}"
|
| 670 |
-
end
|
| 671 |
-
puts "Done!"`,
|
| 672 |
-
|
| 673 |
-
php: `<?php
|
| 674 |
-
echo "Hello from PHP!\\n";
|
| 675 |
-
for ($i = 1; $i <= 5; $i++) {
|
| 676 |
-
echo "Step $i\\n";
|
| 677 |
-
}
|
| 678 |
-
echo "Done!\\n";
|
| 679 |
-
?>`,
|
| 680 |
-
|
| 681 |
-
go: `package main
|
| 682 |
-
import "fmt"
|
| 683 |
-
|
| 684 |
-
func main() {
|
| 685 |
-
fmt.Println("Hello from Go!")
|
| 686 |
-
for i := 1; i <= 5; i++ {
|
| 687 |
-
fmt.Printf("Step %d\\n", i)
|
| 688 |
-
}
|
| 689 |
-
fmt.Println("Done!")
|
| 690 |
-
}`,
|
| 691 |
-
|
| 692 |
-
rust: `fn main() {
|
| 693 |
-
println!("Hello from Rust!");
|
| 694 |
-
for i in 1..=5 {
|
| 695 |
-
println!("Step {}", i);
|
| 696 |
-
}
|
| 697 |
-
println!("Done!");
|
| 698 |
-
}`,
|
| 699 |
-
|
| 700 |
-
cpp: `#include <iostream>
|
| 701 |
-
using namespace std;
|
| 702 |
-
|
| 703 |
-
int main() {
|
| 704 |
-
cout << "Hello from C++!" << endl;
|
| 705 |
-
for (int i = 1; i <= 5; i++) {
|
| 706 |
-
cout << "Step " << i << endl;
|
| 707 |
-
}
|
| 708 |
-
cout << "Done!" << endl;
|
| 709 |
-
return 0;
|
| 710 |
-
}`,
|
| 711 |
-
|
| 712 |
-
c: `#include <stdio.h>
|
| 713 |
-
|
| 714 |
-
int main() {
|
| 715 |
-
printf("Hello from C!\\n");
|
| 716 |
-
for (int i = 1; i <= 5; i++) {
|
| 717 |
-
printf("Step %d\\n", i);
|
| 718 |
-
}
|
| 719 |
-
printf("Done!\\n");
|
| 720 |
-
return 0;
|
| 721 |
-
}`,
|
| 722 |
-
|
| 723 |
-
csharp: `using System;
|
| 724 |
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
Console.WriteLine($"Step {i}");
|
| 730 |
}
|
| 731 |
-
Console.WriteLine("Done!");
|
| 732 |
-
}
|
| 733 |
-
}`,
|
| 734 |
-
|
| 735 |
-
swift: `print("Hello from Swift!")
|
| 736 |
-
for i in 1...5 {
|
| 737 |
-
print("Step \\(i)")
|
| 738 |
-
}
|
| 739 |
-
print("Done!")`,
|
| 740 |
-
|
| 741 |
-
kotlin: `fun main() {
|
| 742 |
-
println("Hello from Kotlin!")
|
| 743 |
-
for (i in 1..5) {
|
| 744 |
-
println("Step $i")
|
| 745 |
-
}
|
| 746 |
-
println("Done!")
|
| 747 |
-
}`,
|
| 748 |
-
|
| 749 |
-
perl: `# Perl example
|
| 750 |
-
print "Hello from Perl!\\n";
|
| 751 |
-
for my $i (1..5) {
|
| 752 |
-
print "Step $i\\n";
|
| 753 |
-
}
|
| 754 |
-
print "Done!\\n";`,
|
| 755 |
-
|
| 756 |
-
lua: `-- Lua example
|
| 757 |
-
print("Hello from Lua!")
|
| 758 |
-
for i = 1, 5 do
|
| 759 |
-
print("Step " .. i)
|
| 760 |
-
end
|
| 761 |
-
print("Done!")`,
|
| 762 |
-
|
| 763 |
-
bash: `#!/bin/bash
|
| 764 |
-
echo "Hello from Bash!"
|
| 765 |
-
for i in {1..5}; do
|
| 766 |
-
echo "Step $i"
|
| 767 |
-
done
|
| 768 |
-
echo "Done!"`,
|
| 769 |
-
|
| 770 |
-
r: `# R example
|
| 771 |
-
print("Hello from R!")
|
| 772 |
-
for (i in 1:5) {
|
| 773 |
-
print(paste("Step", i))
|
| 774 |
-
}
|
| 775 |
-
print("Done!")`,
|
| 776 |
-
|
| 777 |
-
scala: `object Main extends App {
|
| 778 |
-
println("Hello from Scala!")
|
| 779 |
-
for (i <- 1 to 5) {
|
| 780 |
-
println(s"Step $i")
|
| 781 |
-
}
|
| 782 |
-
println("Done!")
|
| 783 |
-
}`,
|
| 784 |
-
|
| 785 |
-
html: `<!DOCTYPE html>
|
| 786 |
-
<html>
|
| 787 |
-
<head>
|
| 788 |
-
<style>
|
| 789 |
-
body { font-family: Arial; padding: 20px; }
|
| 790 |
-
h1 { color: #007acc; }
|
| 791 |
-
</style>
|
| 792 |
-
</head>
|
| 793 |
-
<body>
|
| 794 |
-
<h1>Hello from HTML!</h1>
|
| 795 |
-
<p>This is rendered client-side.</p>
|
| 796 |
-
<button onclick="alert('Clicked!')">Click Me</button>
|
| 797 |
-
</body>
|
| 798 |
-
</html>`,
|
| 799 |
-
|
| 800 |
-
react: `function App() {
|
| 801 |
-
const [count, setCount] = React.useState(0);
|
| 802 |
-
|
| 803 |
-
return (
|
| 804 |
-
<div style={{padding: '20px', fontFamily: 'Arial'}}>
|
| 805 |
-
<h1>Hello from React!</h1>
|
| 806 |
-
<p>Count: {count}</p>
|
| 807 |
-
<button onClick={() => setCount(count + 1)}>
|
| 808 |
-
Increment
|
| 809 |
-
</button>
|
| 810 |
-
</div>
|
| 811 |
-
);
|
| 812 |
-
}
|
| 813 |
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
const
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
output.style.display = 'none';
|
| 823 |
-
htmlPreview.style.display = 'none';
|
| 824 |
-
reactRoot.style.display = 'none';
|
| 825 |
-
|
| 826 |
-
// Show appropriate output area
|
| 827 |
-
if (lang === 'html') {
|
| 828 |
-
htmlPreview.style.display = 'block';
|
| 829 |
-
} else if (lang === 'react') {
|
| 830 |
-
reactRoot.style.display = 'block';
|
| 831 |
-
} else {
|
| 832 |
-
output.style.display = 'block';
|
| 833 |
-
}
|
| 834 |
-
|
| 835 |
-
output.textContent = '';
|
| 836 |
-
status.textContent = '';
|
| 837 |
-
}
|
| 838 |
-
|
| 839 |
-
async function runCode() {
|
| 840 |
-
const lang = languageSelect.value;
|
| 841 |
-
const code = editor.value.trim();
|
| 842 |
-
|
| 843 |
-
if (!code) {
|
| 844 |
-
status.textContent = '⚠️ No code to run';
|
| 845 |
-
status.className = 'error';
|
| 846 |
-
return;
|
| 847 |
-
}
|
| 848 |
-
|
| 849 |
-
output.textContent = '';
|
| 850 |
-
status.textContent = '🚀 Running...';
|
| 851 |
-
status.className = 'info';
|
| 852 |
-
|
| 853 |
-
if (lang === 'html') {
|
| 854 |
-
runHTML(code);
|
| 855 |
-
} else if (lang === 'react') {
|
| 856 |
-
runReact(code);
|
| 857 |
-
} else {
|
| 858 |
-
await runServerSide(lang, code);
|
| 859 |
-
}
|
| 860 |
-
}
|
| 861 |
-
|
| 862 |
-
function runHTML(code) {
|
| 863 |
-
try {
|
| 864 |
-
const clean = DOMPurify.sanitize(code);
|
| 865 |
-
htmlPreview.srcdoc = clean;
|
| 866 |
-
status.textContent = '✓ Rendered';
|
| 867 |
-
status.className = 'success';
|
| 868 |
-
} catch (error) {
|
| 869 |
-
output.style.display = 'block';
|
| 870 |
-
htmlPreview.style.display = 'none';
|
| 871 |
-
output.textContent = 'Error: ' + error.message;
|
| 872 |
-
status.textContent = '❌ Error';
|
| 873 |
-
status.className = 'error';
|
| 874 |
-
}
|
| 875 |
-
}
|
| 876 |
-
|
| 877 |
-
function runReact(code) {
|
| 878 |
-
try {
|
| 879 |
-
reactRoot.innerHTML = '';
|
| 880 |
-
const transformed = Babel.transform(code, {
|
| 881 |
-
presets: ['react']
|
| 882 |
-
}).code;
|
| 883 |
-
eval(transformed);
|
| 884 |
-
status.textContent = '✓ Rendered';
|
| 885 |
-
status.className = 'success';
|
| 886 |
-
} catch (error) {
|
| 887 |
-
reactRoot.innerHTML = '<div style="padding: 20px; color: red; font-family: monospace;">' +
|
| 888 |
-
'<strong>Error:</strong><br>' + error.message + '</div>';
|
| 889 |
-
status.textContent = '❌ Error';
|
| 890 |
-
status.className = 'error';
|
| 891 |
-
}
|
| 892 |
-
}
|
| 893 |
-
|
| 894 |
-
async function runServerSide(lang, code) {
|
| 895 |
runBtn.disabled = true;
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
|
|
|
| 899 |
try {
|
| 900 |
-
const response = await fetch('/
|
| 901 |
method: 'POST',
|
| 902 |
-
headers: {
|
| 903 |
-
body: JSON.stringify({
|
| 904 |
});
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
status.
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
}
|
| 915 |
-
|
| 916 |
-
const reader = response.body.getReader();
|
| 917 |
-
const decoder = new TextDecoder();
|
| 918 |
-
|
| 919 |
-
while (true) {
|
| 920 |
-
const { value, done } = await reader.read();
|
| 921 |
-
if (done) break;
|
| 922 |
-
|
| 923 |
-
const chunk = decoder.decode(value, { stream: true });
|
| 924 |
-
output.textContent += chunk;
|
| 925 |
-
output.scrollTop = output.scrollHeight;
|
| 926 |
|
| 927 |
-
if (
|
| 928 |
-
|
| 929 |
-
|
| 930 |
}
|
| 931 |
}
|
| 932 |
-
|
| 933 |
} catch (error) {
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
status.className = 'error';
|
| 937 |
} finally {
|
| 938 |
runBtn.disabled = false;
|
| 939 |
-
|
| 940 |
}
|
| 941 |
}
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
if (e.key === 'Tab') {
|
| 946 |
-
e.preventDefault();
|
| 947 |
-
const start = editor.selectionStart;
|
| 948 |
-
const end = editor.selectionEnd;
|
| 949 |
-
editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(end);
|
| 950 |
-
editor.selectionStart = editor.selectionEnd = start + 4;
|
| 951 |
-
}
|
| 952 |
-
});
|
| 953 |
-
|
| 954 |
-
// Ctrl+Enter to run
|
| 955 |
-
editor.addEventListener('keydown', (e) => {
|
| 956 |
-
if (e.ctrlKey && e.key === 'Enter') {
|
| 957 |
-
e.preventDefault();
|
| 958 |
-
runCode();
|
| 959 |
-
}
|
| 960 |
});
|
| 961 |
-
|
| 962 |
-
// Initialize
|
| 963 |
-
switchLanguage();
|
| 964 |
</script>
|
| 965 |
</body>
|
| 966 |
</html>
|
| 967 |
"""
|
| 968 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 969 |
|
| 970 |
|
| 971 |
if __name__ == "__main__":
|
|
|
|
| 1 |
"""
|
| 2 |
+
FastAPI Universal Code Execution Sandbox with LANDRUN Security
|
| 3 |
+
Kernel-level sandboxing using Linux Landlock for maximum isolation
|
| 4 |
"""
|
| 5 |
|
| 6 |
from fastapi import FastAPI, Request
|
| 7 |
+
from fastapi.responses import HTMLResponse, JSONResponse
|
| 8 |
from fastapi.middleware.cors import CORSMiddleware
|
| 9 |
+
import subprocess
|
| 10 |
+
import tempfile
|
| 11 |
import os
|
| 12 |
+
import base64
|
| 13 |
+
import shlex
|
| 14 |
|
| 15 |
app = FastAPI()
|
| 16 |
|
| 17 |
+
# Enable CORS
|
| 18 |
app.add_middleware(
|
| 19 |
CORSMiddleware,
|
| 20 |
allow_origins=["*"],
|
|
|
|
| 23 |
allow_headers=["*"],
|
| 24 |
)
|
| 25 |
|
| 26 |
+
def execute_with_landrun(language: str, code: str) -> dict:
|
| 27 |
+
"""Execute code using landrun kernel-level sandboxing"""
|
| 28 |
+
|
| 29 |
+
# Language configurations
|
| 30 |
+
configs = {
|
| 31 |
+
"python": {
|
| 32 |
+
"ext": ".py",
|
| 33 |
+
"cmd": ["python3"],
|
| 34 |
+
"allowed_paths": ["/usr/lib/python3*", "/usr/local/lib/python3*"],
|
| 35 |
+
},
|
| 36 |
+
"javascript": {
|
| 37 |
+
"ext": ".js",
|
| 38 |
+
"cmd": ["node"],
|
| 39 |
+
"allowed_paths": ["/usr/lib/node_modules", "/usr/local/lib/node_modules"],
|
| 40 |
+
},
|
| 41 |
+
"html": {
|
| 42 |
+
"ext": ".html",
|
| 43 |
+
"cmd": None, # Static file
|
| 44 |
+
"allowed_paths": [],
|
| 45 |
+
},
|
| 46 |
+
"react": {
|
| 47 |
+
"ext": ".jsx",
|
| 48 |
+
"cmd": ["node"],
|
| 49 |
+
"allowed_paths": ["/usr/lib/node_modules", "/usr/local/lib/node_modules"],
|
| 50 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
+
config = configs.get(language.lower())
|
| 54 |
if not config:
|
| 55 |
+
return {"error": f"Unsupported language: {language}"}
|
|
|
|
|
|
|
| 56 |
|
| 57 |
+
# Create temporary file
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
try:
|
| 59 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix=config['ext'], delete=False, dir='/tmp/sandbox') as f:
|
| 60 |
f.write(code)
|
| 61 |
+
temp_file = f.name
|
| 62 |
+
|
| 63 |
+
# For HTML/static files, return directly
|
| 64 |
+
if language.lower() == "html":
|
| 65 |
+
with open(temp_file, 'r') as f:
|
| 66 |
+
html_content = f.read()
|
| 67 |
+
os.unlink(temp_file)
|
| 68 |
+
return {
|
| 69 |
+
"output": "HTML rendered successfully",
|
| 70 |
+
"preview": base64.b64encode(html_content.encode()).decode()
|
| 71 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
+
# Build landrun command with security restrictions
|
| 74 |
+
landrun_cmd = [
|
| 75 |
+
"/usr/local/bin/landrun",
|
| 76 |
+
"--ldd", # Auto-detect library dependencies
|
| 77 |
+
"--add-exec", # Auto-add executable
|
| 78 |
+
"--ro", "/usr", # Read-only access to system files
|
| 79 |
+
"--ro", "/lib", # Read-only access to libraries
|
| 80 |
+
"--ro", "/lib64", # Read-only 64-bit libraries
|
| 81 |
+
"--ro", "/etc", # Read-only config (for DNS, etc.)
|
| 82 |
+
"--rw", "/tmp/sandbox", # Write access to sandbox only
|
| 83 |
+
"--ro", temp_file, # Read-only access to code file
|
| 84 |
+
"--connect-tcp", "80,443", # Allow HTTP/HTTPS
|
| 85 |
+
"--log-level", "error",
|
| 86 |
+
]
|
| 87 |
+
|
| 88 |
+
# Add language-specific paths
|
| 89 |
+
for path in config['allowed_paths']:
|
| 90 |
+
landrun_cmd.extend(["--ro", path])
|
| 91 |
+
|
| 92 |
+
# Add execution command
|
| 93 |
+
landrun_cmd.extend(config['cmd'] + [temp_file])
|
| 94 |
+
|
| 95 |
+
# Execute with timeout
|
| 96 |
+
result = subprocess.run(
|
| 97 |
+
landrun_cmd,
|
| 98 |
+
capture_output=True,
|
| 99 |
+
text=True,
|
| 100 |
+
timeout=10,
|
| 101 |
+
cwd="/tmp/sandbox"
|
| 102 |
)
|
| 103 |
|
| 104 |
+
# Clean up
|
| 105 |
+
os.unlink(temp_file)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
+
# Prepare output
|
| 108 |
+
output = result.stdout
|
| 109 |
+
if result.stderr:
|
| 110 |
+
output += f"\n--- STDERR ---\n{result.stderr}"
|
| 111 |
|
| 112 |
+
# For React/JS with output, create preview
|
| 113 |
+
preview = None
|
| 114 |
+
if language.lower() in ["react", "javascript"] and "<" in code:
|
| 115 |
+
preview = base64.b64encode(code.encode()).decode()
|
| 116 |
+
|
| 117 |
+
return {
|
| 118 |
+
"output": output or "Execution completed successfully",
|
| 119 |
+
"exit_code": result.returncode,
|
| 120 |
+
"preview": preview,
|
| 121 |
+
"security": "🔒 Landrun kernel-level isolation active"
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
except subprocess.TimeoutExpired:
|
| 125 |
+
return {"error": "⏱️ Execution timeout (10s limit)"}
|
| 126 |
except Exception as e:
|
| 127 |
+
return {"error": f"❌ Execution error: {str(e)}"}
|
|
|
|
|
|
|
| 128 |
finally:
|
| 129 |
+
# Cleanup temp file if exists
|
| 130 |
+
if 'temp_file' in locals() and os.path.exists(temp_file):
|
| 131 |
+
try:
|
| 132 |
+
os.unlink(temp_file)
|
| 133 |
+
except:
|
| 134 |
+
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
|
| 137 |
@app.get("/", response_class=HTMLResponse)
|
| 138 |
async def root():
|
| 139 |
+
"""Serve the main UI"""
|
| 140 |
+
return """
|
| 141 |
<!DOCTYPE html>
|
| 142 |
<html lang="en">
|
| 143 |
<head>
|
| 144 |
<meta charset="UTF-8">
|
| 145 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 146 |
+
<title>🔒 Landrun Sandbox - Kernel-Level Security</title>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
<style>
|
| 148 |
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
body {
|
| 150 |
+
font-family: 'Segoe UI', system-ui, sans-serif;
|
| 151 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 152 |
+
min-height: 100vh;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
padding: 20px;
|
|
|
|
| 154 |
}
|
| 155 |
+
.container {
|
| 156 |
+
max-width: 1400px;
|
| 157 |
+
margin: 0 auto;
|
| 158 |
+
background: white;
|
| 159 |
+
border-radius: 20px;
|
| 160 |
+
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
| 161 |
+
overflow: hidden;
|
| 162 |
}
|
| 163 |
+
.header {
|
| 164 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 165 |
+
color: white;
|
| 166 |
+
padding: 30px;
|
| 167 |
+
text-align: center;
|
|
|
|
|
|
|
| 168 |
}
|
| 169 |
+
.header h1 { font-size: 2.5em; margin-bottom: 10px; }
|
| 170 |
+
.header p { opacity: 0.9; font-size: 1.1em; }
|
| 171 |
+
.security-badge {
|
| 172 |
+
display: inline-block;
|
| 173 |
+
background: rgba(255,255,255,0.2);
|
| 174 |
+
padding: 8px 16px;
|
| 175 |
+
border-radius: 20px;
|
| 176 |
+
margin-top: 10px;
|
| 177 |
font-weight: bold;
|
| 178 |
}
|
| 179 |
+
.content {
|
| 180 |
+
display: grid;
|
| 181 |
+
grid-template-columns: 1fr 1fr;
|
| 182 |
+
gap: 20px;
|
| 183 |
+
padding: 30px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
}
|
| 185 |
+
.panel {
|
| 186 |
+
background: #f8f9fa;
|
| 187 |
+
border-radius: 12px;
|
| 188 |
+
padding: 20px;
|
| 189 |
}
|
| 190 |
+
.panel h2 {
|
| 191 |
+
color: #667eea;
|
| 192 |
+
margin-bottom: 15px;
|
| 193 |
+
font-size: 1.3em;
|
| 194 |
+
}
|
| 195 |
+
textarea {
|
| 196 |
+
width: 100%;
|
| 197 |
+
height: 300px;
|
| 198 |
+
font-family: 'Monaco', 'Courier New', monospace;
|
| 199 |
+
font-size: 14px;
|
| 200 |
+
padding: 15px;
|
| 201 |
+
border: 2px solid #ddd;
|
| 202 |
+
border-radius: 8px;
|
| 203 |
+
resize: vertical;
|
| 204 |
+
background: white;
|
| 205 |
+
}
|
| 206 |
+
select {
|
| 207 |
+
width: 100%;
|
| 208 |
+
padding: 12px;
|
| 209 |
+
margin-bottom: 15px;
|
| 210 |
+
border: 2px solid #ddd;
|
| 211 |
+
border-radius: 8px;
|
| 212 |
+
font-size: 16px;
|
| 213 |
+
background: white;
|
| 214 |
}
|
|
|
|
| 215 |
button {
|
| 216 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 217 |
color: white;
|
| 218 |
border: none;
|
| 219 |
+
padding: 15px 30px;
|
| 220 |
+
font-size: 16px;
|
| 221 |
font-weight: bold;
|
| 222 |
+
border-radius: 8px;
|
| 223 |
+
cursor: pointer;
|
| 224 |
+
width: 100%;
|
| 225 |
+
margin-top: 10px;
|
| 226 |
+
transition: transform 0.2s;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
}
|
| 228 |
+
button:hover { transform: scale(1.05); }
|
| 229 |
button:disabled {
|
| 230 |
+
background: #ccc;
|
| 231 |
cursor: not-allowed;
|
| 232 |
+
transform: none;
|
| 233 |
}
|
| 234 |
+
.output {
|
| 235 |
+
background: #1e1e1e;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
color: #d4d4d4;
|
| 237 |
+
padding: 20px;
|
| 238 |
+
border-radius: 8px;
|
| 239 |
+
font-family: 'Monaco', 'Courier New', monospace;
|
|
|
|
| 240 |
font-size: 14px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
white-space: pre-wrap;
|
| 242 |
+
min-height: 300px;
|
| 243 |
+
max-height: 500px;
|
| 244 |
+
overflow-y: auto;
|
| 245 |
}
|
| 246 |
+
.preview {
|
| 247 |
+
width: 100%;
|
| 248 |
+
height: 400px;
|
| 249 |
+
border: 2px solid #ddd;
|
| 250 |
+
border-radius: 8px;
|
| 251 |
background: white;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
}
|
| 253 |
+
.status {
|
| 254 |
+
padding: 10px;
|
| 255 |
+
border-radius: 8px;
|
| 256 |
+
margin-bottom: 15px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
font-weight: bold;
|
| 258 |
}
|
| 259 |
+
.status.success {
|
| 260 |
+
background: #d4edda;
|
| 261 |
+
color: #155724;
|
| 262 |
+
border: 1px solid #c3e6cb;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
}
|
| 264 |
+
.status.error {
|
| 265 |
+
background: #f8d7da;
|
| 266 |
+
color: #721c24;
|
| 267 |
+
border: 1px solid #f5c6cb;
|
| 268 |
}
|
| 269 |
+
.examples {
|
| 270 |
+
display: grid;
|
| 271 |
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
| 272 |
+
gap: 10px;
|
| 273 |
+
margin-bottom: 15px;
|
| 274 |
}
|
| 275 |
+
.example-btn {
|
| 276 |
+
padding: 10px;
|
| 277 |
+
background: white;
|
| 278 |
+
border: 2px solid #667eea;
|
| 279 |
+
color: #667eea;
|
| 280 |
+
border-radius: 8px;
|
| 281 |
+
cursor: pointer;
|
| 282 |
+
font-size: 14px;
|
| 283 |
+
transition: all 0.2s;
|
| 284 |
}
|
| 285 |
+
.example-btn:hover {
|
| 286 |
+
background: #667eea;
|
| 287 |
+
color: white;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
}
|
| 289 |
</style>
|
| 290 |
</head>
|
| 291 |
<body>
|
| 292 |
+
<div class="container">
|
| 293 |
+
<div class="header">
|
| 294 |
+
<h1>🔒 Landrun Sandbox</h1>
|
| 295 |
+
<p>Kernel-Level Security with Linux Landlock</p>
|
| 296 |
+
<div class="security-badge">
|
| 297 |
+
🛡️ Maximum Isolation • Zero Trust • Kernel Enforced
|
| 298 |
+
</div>
|
| 299 |
+
</div>
|
| 300 |
|
| 301 |
+
<div class="content">
|
| 302 |
+
<div class="panel">
|
| 303 |
+
<h2>📝 Code Editor</h2>
|
| 304 |
+
<select id="language">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
<option value="python">Python</option>
|
| 306 |
+
<option value="javascript">JavaScript (Node.js)</option>
|
| 307 |
+
<option value="react">React (JSX)</option>
|
| 308 |
+
<option value="html">HTML</option>
|
| 309 |
+
</select>
|
| 310 |
+
|
| 311 |
+
<div class="examples">
|
| 312 |
+
<button class="example-btn" onclick="loadExample('hello')">Hello World</button>
|
| 313 |
+
<button class="example-btn" onclick="loadExample('math')">Math Demo</button>
|
| 314 |
+
<button class="example-btn" onclick="loadExample('html')">HTML Page</button>
|
| 315 |
+
<button class="example-btn" onclick="loadExample('react')">React App</button>
|
| 316 |
+
</div>
|
| 317 |
+
|
| 318 |
+
<textarea id="code" placeholder="Write your code here...">print("Hello from Landrun Sandbox!")
|
| 319 |
+
print("🔒 Running with kernel-level security!")
|
| 320 |
+
import sys
|
| 321 |
+
print(f"Python version: {sys.version}")</textarea>
|
| 322 |
+
|
| 323 |
+
<button id="runBtn" onclick="executeCode()">▶️ Run Code (Landrun Secured)</button>
|
| 324 |
+
</div>
|
|
|
|
| 325 |
|
| 326 |
+
<div class="panel">
|
| 327 |
+
<h2>📺 Output</h2>
|
| 328 |
+
<div id="status"></div>
|
| 329 |
+
<div id="output" class="output">Ready to execute code...</div>
|
| 330 |
+
</div>
|
| 331 |
</div>
|
| 332 |
|
| 333 |
+
<div style="padding: 0 30px 30px 30px;">
|
| 334 |
+
<div class="panel">
|
| 335 |
+
<h2>🖼️ Preview</h2>
|
| 336 |
+
<iframe id="preview" class="preview"></iframe>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
</div>
|
| 338 |
</div>
|
| 339 |
</div>
|
| 340 |
|
| 341 |
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
const examples = {
|
| 343 |
+
hello: {
|
| 344 |
+
python: 'print("Hello from Landrun Sandbox!")\\nprint("🔒 Running with kernel-level security!")',
|
| 345 |
+
javascript: 'console.log("Hello from Landrun Sandbox!");\\nconsole.log("🔒 Running with kernel-level security!");',
|
| 346 |
+
react: 'export default function App() {\\n return <div><h1>Hello from React!</h1><p>🔒 Landrun secured</p></div>;\\n}',
|
| 347 |
+
html: '<!DOCTYPE html>\\n<html>\\n<head><title>Hello</title></head>\\n<body><h1>Hello from HTML!</h1></body>\\n</html>'
|
| 348 |
+
},
|
| 349 |
+
math: {
|
| 350 |
+
python: 'import math\\nprint(f"π = {math.pi}")\\nprint(f"e = {math.e}")\\nprint(f"sqrt(16) = {math.sqrt(16)}")',
|
| 351 |
+
javascript: 'console.log(`π = ${Math.PI}`);\\nconsole.log(`e = ${Math.E}`);\\nconsole.log(`sqrt(16) = ${Math.sqrt(16)}`);'
|
| 352 |
+
},
|
| 353 |
+
html: {
|
| 354 |
+
html: '<!DOCTYPE html>\\n<html>\\n<head><style>body{font-family:Arial;text-align:center;padding:50px}</style></head>\\n<body><h1>🔒 Landrun Sandbox</h1><p>Kernel-level security active!</p></body>\\n</html>'
|
| 355 |
+
},
|
| 356 |
+
react: {
|
| 357 |
+
react: 'export default function App() {\\n return (\\n <div style={{textAlign:"center",padding:"50px"}}>\\n <h1>🔒 Landrun Sandbox</h1>\\n <p>React app with kernel-level security!</p>\\n </div>\\n );\\n}'
|
| 358 |
+
}
|
| 359 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
|
| 361 |
+
function loadExample(type) {
|
| 362 |
+
const lang = document.getElementById('language').value;
|
| 363 |
+
const code = examples[type]?.[lang] || examples[type]?.python || examples.hello[lang];
|
| 364 |
+
document.getElementById('code').value = code;
|
|
|
|
| 365 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
|
| 367 |
+
async function executeCode() {
|
| 368 |
+
const code = document.getElementById('code').value;
|
| 369 |
+
const language = document.getElementById('language').value;
|
| 370 |
+
const output = document.getElementById('output');
|
| 371 |
+
const status = document.getElementById('status');
|
| 372 |
+
const runBtn = document.getElementById('runBtn');
|
| 373 |
+
const preview = document.getElementById('preview');
|
| 374 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
runBtn.disabled = true;
|
| 376 |
+
runBtn.textContent = '⏳ Executing with Landrun...';
|
| 377 |
+
status.innerHTML = '<div class="status">⚙️ Executing in kernel-secured sandbox...</div>';
|
| 378 |
+
output.textContent = 'Executing...';
|
| 379 |
+
|
| 380 |
try {
|
| 381 |
+
const response = await fetch('/execute', {
|
| 382 |
method: 'POST',
|
| 383 |
+
headers: {'Content-Type': 'application/json'},
|
| 384 |
+
body: JSON.stringify({language, code})
|
| 385 |
});
|
| 386 |
+
|
| 387 |
+
const result = await response.json();
|
| 388 |
+
|
| 389 |
+
if (result.error) {
|
| 390 |
+
status.innerHTML = `<div class="status error">❌ Error: ${result.error}</div>`;
|
| 391 |
+
output.textContent = result.error;
|
| 392 |
+
} else {
|
| 393 |
+
status.innerHTML = `<div class="status success">✅ Success! ${result.security || ''}</div>`;
|
| 394 |
+
output.textContent = result.output || 'Execution completed successfully';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
|
| 396 |
+
if (result.preview) {
|
| 397 |
+
const decoded = atob(result.preview);
|
| 398 |
+
preview.srcdoc = decoded;
|
| 399 |
}
|
| 400 |
}
|
|
|
|
| 401 |
} catch (error) {
|
| 402 |
+
status.innerHTML = `<div class="status error">❌ Network Error</div>`;
|
| 403 |
+
output.textContent = error.message;
|
|
|
|
| 404 |
} finally {
|
| 405 |
runBtn.disabled = false;
|
| 406 |
+
runBtn.textContent = '▶️ Run Code (Landrun Secured)';
|
| 407 |
}
|
| 408 |
}
|
| 409 |
+
|
| 410 |
+
document.getElementById('language').addEventListener('change', () => {
|
| 411 |
+
loadExample('hello');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
});
|
|
|
|
|
|
|
|
|
|
| 413 |
</script>
|
| 414 |
</body>
|
| 415 |
</html>
|
| 416 |
"""
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
@app.post("/execute")
|
| 420 |
+
async def execute(request: Request):
|
| 421 |
+
"""Execute code with landrun sandboxing"""
|
| 422 |
+
data = await request.json()
|
| 423 |
+
language = data.get("language", "python")
|
| 424 |
+
code = data.get("code", "")
|
| 425 |
+
|
| 426 |
+
if not code:
|
| 427 |
+
return JSONResponse({"error": "No code provided"})
|
| 428 |
+
|
| 429 |
+
result = execute_with_landrun(language, code)
|
| 430 |
+
return JSONResponse(result)
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
@app.get("/health")
|
| 434 |
+
async def health():
|
| 435 |
+
"""Health check endpoint"""
|
| 436 |
+
return {"status": "healthy", "sandbox": "landrun", "security": "kernel-level"}
|
| 437 |
|
| 438 |
|
| 439 |
if __name__ == "__main__":
|
landrun-main/.github/workflows/build.yml
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Build
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [main]
|
| 6 |
+
pull_request:
|
| 7 |
+
branches: [main]
|
| 8 |
+
|
| 9 |
+
jobs:
|
| 10 |
+
build:
|
| 11 |
+
runs-on: ubuntu-latest
|
| 12 |
+
steps:
|
| 13 |
+
- uses: actions/checkout@v4
|
| 14 |
+
|
| 15 |
+
- name: Set up Go
|
| 16 |
+
uses: actions/setup-go@v5
|
| 17 |
+
with:
|
| 18 |
+
go-version: "1.24.1"
|
| 19 |
+
check-latest: true
|
| 20 |
+
|
| 21 |
+
- name: Install dependencies
|
| 22 |
+
run: go mod download
|
| 23 |
+
|
| 24 |
+
- name: Build
|
| 25 |
+
run: go build -v -o landrun ./cmd/landrun/main.go
|
| 26 |
+
|
| 27 |
+
- name: Upload binary
|
| 28 |
+
uses: actions/upload-artifact@v4
|
| 29 |
+
with:
|
| 30 |
+
name: landrun-linux-amd64
|
| 31 |
+
path: ./landrun
|
landrun-main/.github/workflows/go-compatibility.yml
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Go version compatibility
|
| 2 |
+
|
| 3 |
+
on: [push, pull_request]
|
| 4 |
+
|
| 5 |
+
jobs:
|
| 6 |
+
build:
|
| 7 |
+
runs-on: ubuntu-latest
|
| 8 |
+
strategy:
|
| 9 |
+
matrix:
|
| 10 |
+
go: ["1.18", "1.20", "1.22", "1.24"]
|
| 11 |
+
name: Go ${{ matrix.go }} build
|
| 12 |
+
steps:
|
| 13 |
+
- uses: actions/checkout@v3
|
| 14 |
+
|
| 15 |
+
- name: Set up Go ${{ matrix.go }}
|
| 16 |
+
uses: actions/setup-go@v4
|
| 17 |
+
with:
|
| 18 |
+
go-version: ${{ matrix.go }}
|
| 19 |
+
|
| 20 |
+
- name: Download dependencies
|
| 21 |
+
run: go mod tidy
|
| 22 |
+
|
| 23 |
+
- name: Build landrun
|
| 24 |
+
run: go build ./cmd/landrun
|
landrun-main/.gitignore
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# If you prefer the allow list template instead of the deny list, see community template:
|
| 2 |
+
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
| 3 |
+
#
|
| 4 |
+
# Binaries for programs and plugins
|
| 5 |
+
*.exe
|
| 6 |
+
*.exe~
|
| 7 |
+
*.dll
|
| 8 |
+
*.so
|
| 9 |
+
*.dylib
|
| 10 |
+
|
| 11 |
+
# Test binary, built with `go test -c`
|
| 12 |
+
*.test
|
| 13 |
+
|
| 14 |
+
# Output of the go coverage tool, specifically when used with LiteIDE
|
| 15 |
+
*.out
|
| 16 |
+
|
| 17 |
+
# Dependency directories (remove the comment below to include it)
|
| 18 |
+
# vendor/
|
| 19 |
+
|
| 20 |
+
# Go workspace file
|
| 21 |
+
go.work
|
| 22 |
+
go.work.sum
|
| 23 |
+
|
| 24 |
+
# env file
|
| 25 |
+
.env
|
| 26 |
+
main
|
| 27 |
+
tmp
|
| 28 |
+
internal/sandbox/test_rw
|
| 29 |
+
internal/sandbox/test_ro
|
| 30 |
+
test_env
|
| 31 |
+
landrun
|
landrun-main/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 Armin ranjbar
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
landrun-main/README.md
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Landrun <img src="https://avatars.githubusercontent.com/u/21111839?s=48&v=4" align="right"/>
|
| 2 |
+
|
| 3 |
+
A lightweight, secure sandbox for running Linux processes using Landlock. Think firejail, but with kernel-level security and minimal overhead.
|
| 4 |
+
|
| 5 |
+
Linux Landlock is a kernel-native security module that lets unprivileged processes sandbox themselves.
|
| 6 |
+
|
| 7 |
+
Landrun is designed to make it practical to sandbox any command with fine-grained filesystem and network access controls. No root. No containers. No SELinux/AppArmor configs.
|
| 8 |
+
|
| 9 |
+
It's lightweight, auditable, and wraps Landlock v5 features (file access + TCP restrictions).
|
| 10 |
+
|
| 11 |
+
## Features
|
| 12 |
+
|
| 13 |
+
- 🔒 Kernel-level security using Landlock
|
| 14 |
+
- 🚀 Lightweight and fast execution
|
| 15 |
+
- 🛡️ Fine-grained access control for directories and files
|
| 16 |
+
- 🔄 Support for read and write paths
|
| 17 |
+
- ⚡ Path-specific execution permissions
|
| 18 |
+
- 🌐 TCP network access control (binding and connecting)
|
| 19 |
+
|
| 20 |
+
## Demo
|
| 21 |
+
|
| 22 |
+
<p align="center">
|
| 23 |
+
<img src="demo.gif" alt="landrun demo" width="700"/>
|
| 24 |
+
</p>
|
| 25 |
+
|
| 26 |
+
## Requirements
|
| 27 |
+
|
| 28 |
+
- Linux kernel 5.13 or later with Landlock enabled
|
| 29 |
+
- Linux kernel 6.7 or later for network restrictions (TCP bind/connect)
|
| 30 |
+
- Go 1.18 or later (for building from source)
|
| 31 |
+
|
| 32 |
+
## Installation
|
| 33 |
+
|
| 34 |
+
### Quick Install
|
| 35 |
+
|
| 36 |
+
```bash
|
| 37 |
+
go install github.com/zouuup/landrun/cmd/landrun@latest
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
### From Source
|
| 41 |
+
|
| 42 |
+
```bash
|
| 43 |
+
git clone https://github.com/zouuup/landrun.git
|
| 44 |
+
cd landrun
|
| 45 |
+
go build -o landrun cmd/landrun/main.go
|
| 46 |
+
sudo cp landrun /usr/local/bin/
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
### Distros
|
| 50 |
+
|
| 51 |
+
#### Arch (AUR)
|
| 52 |
+
|
| 53 |
+
- [stable](https://aur.archlinux.org/packages/landrun) maintained by [Vcalv](https://github.com/vcalv)
|
| 54 |
+
- [latest commit](https://aur.archlinux.org/packages/landrun-git) maintained by [juxuanu](https://github.com/juxuanu/)
|
| 55 |
+
|
| 56 |
+
#### Slackware
|
| 57 |
+
|
| 58 |
+
maintained by [r1w1s1](https://github.com/r1w1s1)
|
| 59 |
+
|
| 60 |
+
[Slackbuild](https://slackbuilds.org/repository/15.0/network/landrun/?search=landrun)
|
| 61 |
+
```bash
|
| 62 |
+
sudo sbopkg -i packagename
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
## Usage
|
| 66 |
+
|
| 67 |
+
Basic syntax:
|
| 68 |
+
|
| 69 |
+
```bash
|
| 70 |
+
landrun [options] <command> [args...]
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
### Options
|
| 74 |
+
|
| 75 |
+
- `--ro <path>`: Allow read-only access to specified path (can be specified multiple times or as comma-separated values)
|
| 76 |
+
- `--rox <path>`: Allow read-only access with execution to specified path (can be specified multiple times or as comma-separated values)
|
| 77 |
+
- `--rw <path>`: Allow read-write access to specified path (can be specified multiple times or as comma-separated values)
|
| 78 |
+
- `--rwx <path>`: Allow read-write access with execution to specified path (can be specified multiple times or as comma-separated values)
|
| 79 |
+
- `--bind-tcp <port>`: Allow binding to specified TCP port (can be specified multiple times or as comma-separated values)
|
| 80 |
+
- `--connect-tcp <port>`: Allow connecting to specified TCP port (can be specified multiple times or as comma-separated values)
|
| 81 |
+
- `--env <var>`: Environment variable to pass to the sandboxed command (format: KEY=VALUE or just KEY to pass current value)
|
| 82 |
+
- `--best-effort`: Use best effort mode, falling back to less restrictive sandbox if necessary [default: disabled]
|
| 83 |
+
- `--log-level <level>`: Set logging level (error, info, debug) [default: "error"]
|
| 84 |
+
- `--unrestricted-network`: Allows unrestricted network access (disables all network restrictions)
|
| 85 |
+
- `--unrestricted-filesystem`: Allows unrestricted filesystem access (disables all filesystem restrictions)
|
| 86 |
+
- `--add-exec`: Automatically adds the executing binary to --rox
|
| 87 |
+
- `--ldd`: Automatically adds required libraries to --rox
|
| 88 |
+
|
| 89 |
+
### Important Notes
|
| 90 |
+
|
| 91 |
+
- You must explicitly add the directory or files to the command you want to run with `--rox` flag
|
| 92 |
+
- For system commands, you typically need to include `/usr/bin`, `/usr/lib`, and other system directories
|
| 93 |
+
- Use `--rwx` for directories or files where you need both write access and the ability to execute files
|
| 94 |
+
- Network restrictions require Linux kernel 6.7 or later with Landlock ABI v4
|
| 95 |
+
- By default, no environment variables are passed to the sandboxed command. Use `--env` to explicitly pass environment variables
|
| 96 |
+
- The `--best-effort` flag allows graceful degradation on older kernels that don't support all requested restrictions
|
| 97 |
+
- Paths can be specified either using multiple flags or as comma-separated values (e.g., `--ro /usr,/lib,/home`)
|
| 98 |
+
- If no paths or network rules are specified and neither unrestricted flag is set, landrun will apply maximum restrictions (denying all access)
|
| 99 |
+
|
| 100 |
+
### Environment Variables
|
| 101 |
+
|
| 102 |
+
- `LANDRUN_LOG_LEVEL`: Set logging level (error, info, debug)
|
| 103 |
+
|
| 104 |
+
### Examples
|
| 105 |
+
|
| 106 |
+
1. Run a command that allows exec access to a specific file
|
| 107 |
+
|
| 108 |
+
```bash
|
| 109 |
+
landrun --rox /usr/bin/ls --rox /usr/lib --ro /home ls /home
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
2. Run a command with read-only access to a directory:
|
| 113 |
+
|
| 114 |
+
```bash
|
| 115 |
+
landrun --rox /usr/ --ro /path/to/dir ls /path/to/dir
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
3. Run a command with write access to a directory:
|
| 119 |
+
|
| 120 |
+
```bash
|
| 121 |
+
landrun --rox /usr/bin --ro /lib --rw /path/to/dir touch /path/to/dir/newfile
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
4. Run a command with write access to a file:
|
| 125 |
+
|
| 126 |
+
```bash
|
| 127 |
+
landrun --rox /usr/bin --ro /lib --rw /path/to/dir/newfile touch /path/to/dir/newfile
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
5. Run a command with execution permissions:
|
| 131 |
+
|
| 132 |
+
```bash
|
| 133 |
+
landrun --rox /usr/ --ro /lib,/lib64 /usr/bin/bash
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
6. Run with debug logging:
|
| 137 |
+
|
| 138 |
+
```bash
|
| 139 |
+
landrun --log-level debug --rox /usr/ --ro /lib,/lib64,/path/to/dir ls /path/to/dir
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
7. Run with network restrictions:
|
| 143 |
+
|
| 144 |
+
```bash
|
| 145 |
+
landrun --rox /usr/ --ro /lib,/lib64 --bind-tcp 8080 --connect-tcp 80 /usr/bin/my-server
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
This will allow the program to only bind to TCP port 8080 and connect to TCP port 80.
|
| 149 |
+
|
| 150 |
+
8. Run a DNS client with appropriate permissions:
|
| 151 |
+
|
| 152 |
+
```bash
|
| 153 |
+
landrun --log-level debug --ro /etc,/usr --rox /usr/ --connect-tcp 443 nc kernel.org 443
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
This allows connections to port 443, requires access to /etc/resolv.conf for resolving DNS.
|
| 157 |
+
|
| 158 |
+
9. Run a web server with selective network permissions:
|
| 159 |
+
|
| 160 |
+
```bash
|
| 161 |
+
landrun --rox /usr/bin --ro /lib,/lib64,/var/www --rwx /var/log --bind-tcp 80,443 /usr/bin/nginx
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
10. Running anything without providing parameters is... maximum security jail!
|
| 165 |
+
|
| 166 |
+
```bash
|
| 167 |
+
landrun ls
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
11. If you keep getting permission denied without knowing what exactly going on, best to use strace with it.
|
| 171 |
+
|
| 172 |
+
```bash
|
| 173 |
+
landrun --rox /usr strace -f -e trace=all ls
|
| 174 |
+
```
|
| 175 |
+
|
| 176 |
+
12. Run with specific environment variables:
|
| 177 |
+
|
| 178 |
+
```bash
|
| 179 |
+
landrun --rox /usr --ro /etc --env HOME --env PATH --env CUSTOM_VAR=my_value -- env
|
| 180 |
+
```
|
| 181 |
+
|
| 182 |
+
This example passes the current HOME and PATH variables, plus a custom variable named CUSTOM_VAR.
|
| 183 |
+
|
| 184 |
+
13. Run command with explicity access to files instead of directories:
|
| 185 |
+
```bash
|
| 186 |
+
landrun --rox /usr/lib/libc.so.6 --rox /usr/lib64/ld-linux-x86-64.so.2 --rox /usr/bin/true /usr/bin/true
|
| 187 |
+
```
|
| 188 |
+
|
| 189 |
+
14. Run a command with --add-exec which automatically adds target binary to --rox
|
| 190 |
+
|
| 191 |
+
```bash
|
| 192 |
+
landrun --rox /usr/lib/ --add-exec /usr/bin/true
|
| 193 |
+
```
|
| 194 |
+
|
| 195 |
+
15. Run a command with --ldd and --add-exec which automatically adds required libraries and target binary to --rox
|
| 196 |
+
|
| 197 |
+
```bash
|
| 198 |
+
landrun --ldd --add-exec /usr/bin/true
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
Note that shared libs always need exec permission due to how they are loaded, PROT_EXEC on mmap() etc.
|
| 202 |
+
|
| 203 |
+
## Systemd Integration
|
| 204 |
+
|
| 205 |
+
landrun can be integrated with systemd to run services with enhanced security. Here's an example of running nginx with landrun:
|
| 206 |
+
|
| 207 |
+
1. Create a systemd service file (e.g., `/etc/systemd/system/nginx-landrun.service`):
|
| 208 |
+
|
| 209 |
+
```ini
|
| 210 |
+
[Unit]
|
| 211 |
+
Description=nginx with landrun sandbox
|
| 212 |
+
After=network.target
|
| 213 |
+
|
| 214 |
+
[Service]
|
| 215 |
+
Type=simple
|
| 216 |
+
ExecStart=/usr/bin/landrun \
|
| 217 |
+
--rox /usr/bin,/usr/lib \
|
| 218 |
+
--ro /etc/nginx,/etc/ssl,/etc/passwd,/etc/group,/etc/nsswitch.conf \
|
| 219 |
+
--rwx /var/log/nginx \
|
| 220 |
+
--rwx /var/cache/nginx \
|
| 221 |
+
--bind-tcp 80,443 \
|
| 222 |
+
/usr/bin/nginx -g 'daemon off;'
|
| 223 |
+
Restart=always
|
| 224 |
+
User=nginx
|
| 225 |
+
Group=nginx
|
| 226 |
+
|
| 227 |
+
[Install]
|
| 228 |
+
WantedBy=multi-user.target
|
| 229 |
+
```
|
| 230 |
+
|
| 231 |
+
2. Enable and start the service:
|
| 232 |
+
|
| 233 |
+
```bash
|
| 234 |
+
sudo systemctl daemon-reload
|
| 235 |
+
sudo systemctl enable nginx-landrun
|
| 236 |
+
sudo systemctl start nginx-landrun
|
| 237 |
+
```
|
| 238 |
+
|
| 239 |
+
3. Check the service status:
|
| 240 |
+
|
| 241 |
+
```bash
|
| 242 |
+
sudo systemctl status nginx-landrun
|
| 243 |
+
```
|
| 244 |
+
|
| 245 |
+
This configuration:
|
| 246 |
+
- Runs nginx with minimal required permissions
|
| 247 |
+
- Allows binding to ports 80 and 443
|
| 248 |
+
- Provides read-only access to configuration files
|
| 249 |
+
- Allows write access only to log and cache directories
|
| 250 |
+
- Runs as the nginx user and group
|
| 251 |
+
- Automatically restarts on failure
|
| 252 |
+
|
| 253 |
+
You can adjust the permissions based on your specific needs. For example, if you need to serve static files from `/var/www`, add `--ro /var/www` to the ExecStart line.
|
| 254 |
+
|
| 255 |
+
## Security
|
| 256 |
+
|
| 257 |
+
landrun uses Linux's Landlock to create a secure sandbox environment. It provides:
|
| 258 |
+
|
| 259 |
+
- File system access control
|
| 260 |
+
- Directory access restrictions
|
| 261 |
+
- Execution control
|
| 262 |
+
- TCP network restrictions
|
| 263 |
+
- Process isolation
|
| 264 |
+
- Default restrictive mode when no rules are specified
|
| 265 |
+
|
| 266 |
+
Landlock is an access-control system that enables processes to securely restrict themselves and their future children. As a stackable Linux Security Module (LSM), it creates additional security layers on top of existing system-wide access controls, helping to mitigate security impacts from bugs or malicious behavior in applications.
|
| 267 |
+
|
| 268 |
+
### Landlock Access Control Rights
|
| 269 |
+
|
| 270 |
+
landrun leverages Landlock's fine-grained access control mechanisms, which include:
|
| 271 |
+
|
| 272 |
+
**File-specific rights:**
|
| 273 |
+
|
| 274 |
+
- Execute files (`LANDLOCK_ACCESS_FS_EXECUTE`)
|
| 275 |
+
- Write to files (`LANDLOCK_ACCESS_FS_WRITE_FILE`)
|
| 276 |
+
- Read files (`LANDLOCK_ACCESS_FS_READ_FILE`)
|
| 277 |
+
- Truncate files (`LANDLOCK_ACCESS_FS_TRUNCATE`) - Available since Landlock ABI v3
|
| 278 |
+
- IOCTL operations on devices (`LANDLOCK_ACCESS_FS_IOCTL_DEV`) - Available since Landlock ABI v5
|
| 279 |
+
|
| 280 |
+
**Directory-specific rights:**
|
| 281 |
+
|
| 282 |
+
- Read directory contents (`LANDLOCK_ACCESS_FS_READ_DIR`)
|
| 283 |
+
- Remove directories (`LANDLOCK_ACCESS_FS_REMOVE_DIR`)
|
| 284 |
+
- Remove files (`LANDLOCK_ACCESS_FS_REMOVE_FILE`)
|
| 285 |
+
- Create various filesystem objects (char devices, directories, regular files, sockets, etc.)
|
| 286 |
+
- Refer/reparent files across directories (`LANDLOCK_ACCESS_FS_REFER`) - Available since Landlock ABI v2
|
| 287 |
+
|
| 288 |
+
**Network-specific rights** (requires Linux 6.7+ with Landlock ABI v4):
|
| 289 |
+
|
| 290 |
+
- Bind to specific TCP ports (`LANDLOCK_ACCESS_NET_BIND_TCP`)
|
| 291 |
+
- Connect to specific TCP ports (`LANDLOCK_ACCESS_NET_CONNECT_TCP`)
|
| 292 |
+
|
| 293 |
+
### Limitations
|
| 294 |
+
|
| 295 |
+
- Landlock must be supported by your kernel
|
| 296 |
+
- Network restrictions require Linux kernel 6.7 or later with Landlock ABI v4
|
| 297 |
+
- Some operations may require additional permissions
|
| 298 |
+
- Files or directories opened before sandboxing are not subject to Landlock restrictions
|
| 299 |
+
|
| 300 |
+
## Kernel Compatibility Table
|
| 301 |
+
|
| 302 |
+
| Feature | Minimum Kernel Version | Landlock ABI Version |
|
| 303 |
+
| ---------------------------------- | ---------------------- | -------------------- |
|
| 304 |
+
| Basic filesystem sandboxing | 5.13 | 1 |
|
| 305 |
+
| File referring/reparenting control | 5.19 | 2 |
|
| 306 |
+
| File truncation control | 6.2 | 3 |
|
| 307 |
+
| Network TCP restrictions | 6.7 | 4 |
|
| 308 |
+
| IOCTL on special files | 6.10 | 5 |
|
| 309 |
+
|
| 310 |
+
## Troubleshooting
|
| 311 |
+
|
| 312 |
+
If you receive "permission denied" or similar errors:
|
| 313 |
+
|
| 314 |
+
1. Ensure you've added all necessary paths with `--ro` or `--rw`
|
| 315 |
+
2. Try running with `--log-level debug` to see detailed permission information
|
| 316 |
+
3. Check that Landlock is supported and enabled on your system:
|
| 317 |
+
```bash
|
| 318 |
+
grep -E 'landlock|lsm=' /boot/config-$(uname -r)
|
| 319 |
+
# alternatively, if there are no /boot/config-* files
|
| 320 |
+
zgrep -iE 'landlock|lsm=' /proc/config.gz
|
| 321 |
+
# another alternate method
|
| 322 |
+
grep -iE 'landlock|lsm=' /lib/modules/$(uname -r)/config
|
| 323 |
+
```
|
| 324 |
+
You should see `CONFIG_SECURITY_LANDLOCK=y` and `lsm=landlock,...` in the output
|
| 325 |
+
4. For network restrictions, verify your kernel version is 6.7+ with Landlock ABI v4:
|
| 326 |
+
```bash
|
| 327 |
+
uname -r
|
| 328 |
+
```
|
| 329 |
+
|
| 330 |
+
## Technical Details
|
| 331 |
+
|
| 332 |
+
### Implementation
|
| 333 |
+
|
| 334 |
+
This project uses the [landlock-lsm/go-landlock](https://github.com/landlock-lsm/go-landlock) package for sandboxing, which provides both filesystem and network restrictions. The current implementation supports:
|
| 335 |
+
|
| 336 |
+
- Read/write/execute restrictions for files and directories
|
| 337 |
+
- TCP port binding restrictions
|
| 338 |
+
- TCP port connection restrictions
|
| 339 |
+
- Best-effort mode for graceful degradation on older kernels
|
| 340 |
+
|
| 341 |
+
### Best-Effort Mode
|
| 342 |
+
|
| 343 |
+
When using `--best-effort` (disabled by default), landrun will gracefully degrade to using the best available Landlock version on the current kernel. This means:
|
| 344 |
+
|
| 345 |
+
- On Linux 6.7+: Full filesystem and network restrictions
|
| 346 |
+
- On Linux 6.2-6.6: Filesystem restrictions including truncation, but no network restrictions
|
| 347 |
+
- On Linux 5.19-6.1: Basic filesystem restrictions including file reparenting, but no truncation control or network restrictions
|
| 348 |
+
- On Linux 5.13-5.18: Basic filesystem restrictions without file reparenting, truncation control, or network restrictions
|
| 349 |
+
- On older Linux: No restrictions (sandbox disabled)
|
| 350 |
+
|
| 351 |
+
When no rules are specified and neither unrestricted flag is set, landrun will apply maximum restrictions available for the current kernel version.
|
| 352 |
+
|
| 353 |
+
### Tests
|
| 354 |
+
|
| 355 |
+
The project includes a comprehensive test suite that verifies:
|
| 356 |
+
|
| 357 |
+
- Basic filesystem access controls (read-only, read-write, execute)
|
| 358 |
+
- Directory traversal and path handling
|
| 359 |
+
- Network restrictions (TCP bind/connect)
|
| 360 |
+
- Environment variable isolation
|
| 361 |
+
- System command execution
|
| 362 |
+
- Edge cases and regression tests
|
| 363 |
+
|
| 364 |
+
Run the tests with:
|
| 365 |
+
|
| 366 |
+
```bash
|
| 367 |
+
./test.sh
|
| 368 |
+
```
|
| 369 |
+
|
| 370 |
+
Use `--keep-binary` to preserve the test binary after completion:
|
| 371 |
+
|
| 372 |
+
```bash
|
| 373 |
+
./test.sh --keep-binary
|
| 374 |
+
```
|
| 375 |
+
|
| 376 |
+
Use `--use-system` to test against the system-installed landrun binary:
|
| 377 |
+
|
| 378 |
+
```bash
|
| 379 |
+
./test.sh --use-system
|
| 380 |
+
```
|
| 381 |
+
|
| 382 |
+
## Future Features
|
| 383 |
+
|
| 384 |
+
Based on the Linux Landlock API capabilities, we plan to add:
|
| 385 |
+
|
| 386 |
+
- 🔒 Enhanced filesystem controls with more fine-grained permissions
|
| 387 |
+
- 🌐 Support for UDP and other network protocol restrictions (when supported by Linux kernel)
|
| 388 |
+
- 🔄 Process scoping and resource controls
|
| 389 |
+
- 🛡️ Additional security features as they become available in the Landlock API
|
| 390 |
+
|
| 391 |
+
## Acknowledgements
|
| 392 |
+
|
| 393 |
+
This project wouldn't exist without:
|
| 394 |
+
|
| 395 |
+
- [Landlock](https://landlock.io), the kernel security module enabling unprivileged sandboxing - maintained by [@l0kod](https://github.com/l0kod)
|
| 396 |
+
- [go-landlock](https://github.com/landlock-lsm/go-landlock), the Go bindings powering this tool - developed by [@gnoack](https://github.com/gnoack)
|
| 397 |
+
|
| 398 |
+
## Contributing
|
| 399 |
+
|
| 400 |
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
landrun-main/demo.gif
ADDED
|
landrun-main/go.mod
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module github.com/zouuup/landrun
|
| 2 |
+
|
| 3 |
+
go 1.18
|
| 4 |
+
|
| 5 |
+
require (
|
| 6 |
+
github.com/landlock-lsm/go-landlock v0.0.0-20250303204525-1544bccde3a3
|
| 7 |
+
github.com/urfave/cli/v2 v2.27.6
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
require (
|
| 11 |
+
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
| 12 |
+
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
| 13 |
+
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
| 14 |
+
golang.org/x/sys v0.26.0 // indirect
|
| 15 |
+
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 // indirect
|
| 16 |
+
)
|
landrun-main/go.sum
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
| 2 |
+
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
| 3 |
+
github.com/landlock-lsm/go-landlock v0.0.0-20250303204525-1544bccde3a3 h1:zcMi8R8vP0WrrXlFMNUBpDy/ydo3sTnCcUPowq1XmSc=
|
| 4 |
+
github.com/landlock-lsm/go-landlock v0.0.0-20250303204525-1544bccde3a3/go.mod h1:RSub3ourNF8Hf+swvw49Catm3s7HVf4hzdFxDUnEzdA=
|
| 5 |
+
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
| 6 |
+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
| 7 |
+
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
|
| 8 |
+
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
| 9 |
+
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
| 10 |
+
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
| 11 |
+
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
| 12 |
+
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
| 13 |
+
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70 h1:HsB2G/rEQiYyo1bGoQqHZ/Bvd6x1rERQTNdPr1FyWjI=
|
| 14 |
+
kernel.org/pub/linux/libs/security/libcap/psx v1.2.70/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
|
landrun-main/internal/elfdeps/elfdeps.go
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package elfdeps
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"debug/elf"
|
| 5 |
+
"io"
|
| 6 |
+
"os"
|
| 7 |
+
osexec "os/exec"
|
| 8 |
+
"path/filepath"
|
| 9 |
+
"strings"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
// ldconfigRunner runs `ldconfig -p` and returns its output. Tests may override
|
| 13 |
+
// this variable to inject fake output. It is unexported on purpose to allow
|
| 14 |
+
// test injection within the package.
|
| 15 |
+
var ldconfigRunner = func() ([]byte, error) {
|
| 16 |
+
return osexec.Command("ldconfig", "-p").Output()
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
// getLdmap runs `ldconfig -p` and returns a map of soname -> path.
|
| 20 |
+
func getLdmap() map[string]string {
|
| 21 |
+
m := map[string]string{}
|
| 22 |
+
out, err := ldconfigRunner()
|
| 23 |
+
if err != nil {
|
| 24 |
+
return m
|
| 25 |
+
}
|
| 26 |
+
lines := strings.Split(string(out), "\n")
|
| 27 |
+
for _, line := range lines {
|
| 28 |
+
if !strings.Contains(line, "=>") {
|
| 29 |
+
continue
|
| 30 |
+
}
|
| 31 |
+
parts := strings.Split(line, "=>")
|
| 32 |
+
if len(parts) < 2 {
|
| 33 |
+
continue
|
| 34 |
+
}
|
| 35 |
+
path := strings.TrimSpace(parts[len(parts)-1])
|
| 36 |
+
left := strings.TrimSpace(parts[0])
|
| 37 |
+
toks := strings.Fields(left)
|
| 38 |
+
if len(toks) == 0 {
|
| 39 |
+
continue
|
| 40 |
+
}
|
| 41 |
+
soname := toks[0]
|
| 42 |
+
if path == "" || soname == "" {
|
| 43 |
+
continue
|
| 44 |
+
}
|
| 45 |
+
if _, err := os.Stat(path); err == nil {
|
| 46 |
+
if _, exists := m[soname]; !exists {
|
| 47 |
+
m[soname] = path
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
return m
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// parseInterp extracts the PT_INTERP interpreter path from an ELF file.
|
| 55 |
+
func parseInterp(f *elf.File) string {
|
| 56 |
+
for _, prog := range f.Progs {
|
| 57 |
+
if prog.Type == elf.PT_INTERP {
|
| 58 |
+
r := prog.Open()
|
| 59 |
+
if r == nil {
|
| 60 |
+
// Can't read interpreter
|
| 61 |
+
return ""
|
| 62 |
+
}
|
| 63 |
+
if data, err := io.ReadAll(r); err == nil {
|
| 64 |
+
return strings.TrimRight(string(data), "\x00")
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
return ""
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
// parseDynamic extracts DT_NEEDED and RPATH/RUNPATH entries from the .dynamic section.
|
| 72 |
+
func parseDynamic(f *elf.File) (needed []string, rpaths []string) {
|
| 73 |
+
needed = []string{}
|
| 74 |
+
rpaths = []string{}
|
| 75 |
+
|
| 76 |
+
if libs, err := f.DynString(elf.DT_NEEDED); err == nil {
|
| 77 |
+
needed = append(needed, libs...)
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// DT_RPATH and DT_RUNPATH may both be present; split on ':' and append
|
| 81 |
+
if rp, err := f.DynString(elf.DT_RPATH); err == nil {
|
| 82 |
+
for _, v := range rp {
|
| 83 |
+
if v == "" {
|
| 84 |
+
continue
|
| 85 |
+
}
|
| 86 |
+
rpaths = append(rpaths, strings.Split(v, ":")...)
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
if rp, err := f.DynString(elf.DT_RUNPATH); err == nil {
|
| 90 |
+
for _, v := range rp {
|
| 91 |
+
if v == "" {
|
| 92 |
+
continue
|
| 93 |
+
}
|
| 94 |
+
rpaths = append(rpaths, strings.Split(v, ":")...)
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
return
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// normalizeRpaths expands common tokens like $ORIGIN and makes relative
|
| 101 |
+
// rpath entries absolute using the provided origin directory.
|
| 102 |
+
func normalizeRpaths(rpaths []string, origin string) []string {
|
| 103 |
+
out := []string{}
|
| 104 |
+
for _, rp := range rpaths {
|
| 105 |
+
if rp == "" {
|
| 106 |
+
continue
|
| 107 |
+
}
|
| 108 |
+
// expand $ORIGIN (common token in RPATH/RUNPATH)
|
| 109 |
+
rp = strings.ReplaceAll(rp, "$ORIGIN", origin)
|
| 110 |
+
rp = strings.ReplaceAll(rp, "${ORIGIN}", origin)
|
| 111 |
+
// make relative rpath entries absolute using origin
|
| 112 |
+
if !filepath.IsAbs(rp) {
|
| 113 |
+
rp = filepath.Join(origin, rp)
|
| 114 |
+
}
|
| 115 |
+
out = append(out, rp)
|
| 116 |
+
}
|
| 117 |
+
return out
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
// resolveSingleSoname attempts to resolve a single soname using rpaths,
|
| 121 |
+
// standard dirs and ldconfig fallback. It takes a pointer to ldmap so the
|
| 122 |
+
// caller can lazily populate and reuse it.
|
| 123 |
+
func resolveSingleSoname(soname string, rpaths []string, stdDirs []string, ldmap *map[string]string) string {
|
| 124 |
+
// check rpaths first
|
| 125 |
+
for _, rp := range rpaths {
|
| 126 |
+
candidate := filepath.Join(rp, soname)
|
| 127 |
+
if _, err := os.Stat(candidate); err == nil {
|
| 128 |
+
return candidate
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
// then check standard dirs
|
| 133 |
+
for _, d := range stdDirs {
|
| 134 |
+
candidate := filepath.Join(d, soname)
|
| 135 |
+
if _, err := os.Stat(candidate); err == nil {
|
| 136 |
+
return candidate
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
// fallback: consult parsed ldconfig map (populate lazily)
|
| 141 |
+
if *ldmap == nil {
|
| 142 |
+
*ldmap = getLdmap()
|
| 143 |
+
}
|
| 144 |
+
if p, ok := (*ldmap)[soname]; ok {
|
| 145 |
+
return p
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
return ""
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
// resolveSonames attempts to resolve sonames to absolute paths using rpaths,
|
| 152 |
+
// standard library directories and falling back to parsing `ldconfig -p` output.
|
| 153 |
+
func resolveSonames(needed []string, rpaths []string) []string {
|
| 154 |
+
resolved := map[string]string{}
|
| 155 |
+
stdDirs := []string{"/lib", "/lib64", "/usr/lib", "/usr/lib64", "/usr/local/lib"}
|
| 156 |
+
var ldmap map[string]string
|
| 157 |
+
|
| 158 |
+
for _, soname := range needed {
|
| 159 |
+
if _, ok := resolved[soname]; ok {
|
| 160 |
+
continue
|
| 161 |
+
}
|
| 162 |
+
resolved[soname] = resolveSingleSoname(soname, rpaths, stdDirs, &ldmap)
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
out := []string{}
|
| 166 |
+
for _, r := range resolved {
|
| 167 |
+
if r != "" {
|
| 168 |
+
out = append(out, r)
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
return out
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
// GetLibraryDependencies returns a list of library paths that the given binary depends on
|
| 175 |
+
func GetLibraryDependencies(binary string) ([]string, error) {
|
| 176 |
+
queue := []string{binary}
|
| 177 |
+
processed := map[string]struct{}{}
|
| 178 |
+
finalMap := map[string]struct{}{}
|
| 179 |
+
|
| 180 |
+
// Add /etc/ld.so.cache if present
|
| 181 |
+
if _, err := os.Stat("/etc/ld.so.cache"); err == nil {
|
| 182 |
+
finalMap["/etc/ld.so.cache"] = struct{}{}
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
for len(queue) > 0 {
|
| 186 |
+
// Dequeue
|
| 187 |
+
curr := queue[0]
|
| 188 |
+
queue = queue[1:]
|
| 189 |
+
|
| 190 |
+
if _, ok := processed[curr]; ok {
|
| 191 |
+
continue
|
| 192 |
+
}
|
| 193 |
+
processed[curr] = struct{}{}
|
| 194 |
+
|
| 195 |
+
f, err := elf.Open(curr)
|
| 196 |
+
if err != nil {
|
| 197 |
+
// This can happen with non-ELF files in the dependency chain
|
| 198 |
+
// (e.g. ld.so.cache). Ignore them.
|
| 199 |
+
continue
|
| 200 |
+
}
|
| 201 |
+
defer f.Close()
|
| 202 |
+
|
| 203 |
+
// The first binary in the queue is the main one; grab its interpreter
|
| 204 |
+
if curr == binary {
|
| 205 |
+
if interpPath := parseInterp(f); interpPath != "" {
|
| 206 |
+
finalMap[interpPath] = struct{}{}
|
| 207 |
+
queue = append(queue, interpPath)
|
| 208 |
+
}
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
needed, rpaths := parseDynamic(f)
|
| 212 |
+
origin := filepath.Dir(curr)
|
| 213 |
+
rpaths = normalizeRpaths(rpaths, origin)
|
| 214 |
+
libPaths := resolveSonames(needed, rpaths)
|
| 215 |
+
|
| 216 |
+
for _, p := range libPaths {
|
| 217 |
+
if _, ok := finalMap[p]; !ok {
|
| 218 |
+
finalMap[p] = struct{}{}
|
| 219 |
+
queue = append(queue, p)
|
| 220 |
+
}
|
| 221 |
+
}
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
out := make([]string, 0, len(finalMap))
|
| 225 |
+
for p := range finalMap {
|
| 226 |
+
out = append(out, p)
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
return out, nil
|
| 230 |
+
}
|
landrun-main/internal/elfdeps/elfdeps_test.go
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package elfdeps
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"debug/elf"
|
| 5 |
+
"os"
|
| 6 |
+
"os/exec"
|
| 7 |
+
"path/filepath"
|
| 8 |
+
"testing"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
// Test helpers against a known binary in the system: find `true` via LookPath
|
| 12 |
+
func TestParseAndResolveTrue(t *testing.T) {
|
| 13 |
+
bin, err := exec.LookPath("true")
|
| 14 |
+
if err != nil {
|
| 15 |
+
t.Fatalf("failed to find 'true' binary: %v", err)
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
f, err := elf.Open(bin)
|
| 19 |
+
if err != nil {
|
| 20 |
+
t.Fatalf("failed to open %s: %v", bin, err)
|
| 21 |
+
}
|
| 22 |
+
defer f.Close()
|
| 23 |
+
|
| 24 |
+
interp := parseInterp(f)
|
| 25 |
+
if interp == "" {
|
| 26 |
+
t.Fatalf("expected interpreter for %s, got empty", bin)
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
needed, rpaths := parseDynamic(f)
|
| 30 |
+
if needed == nil {
|
| 31 |
+
needed = []string{}
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
origin := filepath.Dir(bin)
|
| 35 |
+
rpaths = normalizeRpaths(rpaths, origin)
|
| 36 |
+
paths := resolveSonames(needed, rpaths)
|
| 37 |
+
if paths == nil {
|
| 38 |
+
paths = []string{}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// Ensure interpreter path exists on filesystem
|
| 42 |
+
if _, err := os.Stat(interp); err != nil {
|
| 43 |
+
t.Fatalf("interp path %s does not exist: %v", interp, err)
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// If there are resolved library paths, they must exist
|
| 47 |
+
for _, p := range paths {
|
| 48 |
+
if _, err := os.Stat(p); err != nil {
|
| 49 |
+
t.Fatalf("resolved library path %s does not exist: %v", p, err)
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
func TestRecursiveDependencies(t *testing.T) {
|
| 55 |
+
if _, err := exec.LookPath("gcc"); err != nil {
|
| 56 |
+
t.Skip("gcc not found, skipping test")
|
| 57 |
+
}
|
| 58 |
+
// Create a temporary directory for compiled artifacts
|
| 59 |
+
tempDir := t.TempDir()
|
| 60 |
+
|
| 61 |
+
// Compile liba.so
|
| 62 |
+
libaSrc := "testdata/liba.c"
|
| 63 |
+
libaSo := filepath.Join(tempDir, "liba.so")
|
| 64 |
+
cmd := exec.Command("gcc", "-fPIC", "-shared", "-o", libaSo, libaSrc)
|
| 65 |
+
if out, err := cmd.CombinedOutput(); err != nil {
|
| 66 |
+
t.Fatalf("failed to compile liba.so: %v\n%s", err, string(out))
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
// Compile libb.so
|
| 70 |
+
libbSrc := "testdata/libb.c"
|
| 71 |
+
libbSo := filepath.Join(tempDir, "libb.so")
|
| 72 |
+
cmd = exec.Command("gcc", "-fPIC", "-shared", "-o", libbSo, libbSrc, "-L"+tempDir, "-la", "-Wl,-rpath,$ORIGIN")
|
| 73 |
+
if out, err := cmd.CombinedOutput(); err != nil {
|
| 74 |
+
t.Fatalf("failed to compile libb.so: %v\n%s", err, string(out))
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// Compile test_binary
|
| 78 |
+
mainSrc := "testdata/main.c"
|
| 79 |
+
testBin := filepath.Join(tempDir, "test_binary")
|
| 80 |
+
cmd = exec.Command("gcc", "-o", testBin, mainSrc, "-L"+tempDir, "-lb", "-Wl,-rpath,$ORIGIN")
|
| 81 |
+
if out, err := cmd.CombinedOutput(); err != nil {
|
| 82 |
+
t.Fatalf("failed to compile test_binary: %v\n%s", err, string(out))
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
// Run the actual test logic
|
| 86 |
+
deps, err := GetLibraryDependencies(testBin)
|
| 87 |
+
if err != nil {
|
| 88 |
+
t.Fatalf("GetLibraryDependencies failed: %v", err)
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
foundA := false
|
| 92 |
+
foundB := false
|
| 93 |
+
for _, dep := range deps {
|
| 94 |
+
if dep == libaSo {
|
| 95 |
+
foundA = true
|
| 96 |
+
}
|
| 97 |
+
if dep == libbSo {
|
| 98 |
+
foundB = true
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
if !foundA {
|
| 103 |
+
t.Errorf("expected to find %s in dependency list, but didn't. Found: %v", libaSo, deps)
|
| 104 |
+
}
|
| 105 |
+
if !foundB {
|
| 106 |
+
t.Errorf("expected to find %s in dependency list, but didn't. Found: %v", libbSo, deps)
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
func TestGetLibraryDependencies(t *testing.T) {
|
| 111 |
+
bin, err := exec.LookPath("true")
|
| 112 |
+
if err != nil {
|
| 113 |
+
t.Fatalf("failed to find 'true' binary: %v", err)
|
| 114 |
+
}
|
| 115 |
+
paths, err := GetLibraryDependencies(bin)
|
| 116 |
+
if err != nil {
|
| 117 |
+
t.Fatalf("GetLibraryDependencies failed: %v", err)
|
| 118 |
+
}
|
| 119 |
+
if len(paths) == 0 {
|
| 120 |
+
t.Fatalf("expected non-empty dependency list for %s", bin)
|
| 121 |
+
}
|
| 122 |
+
// ensure returned paths are absolute and exist
|
| 123 |
+
for _, p := range paths {
|
| 124 |
+
if !filepath.IsAbs(p) {
|
| 125 |
+
t.Fatalf("expected absolute path, got %s", p)
|
| 126 |
+
}
|
| 127 |
+
if _, err := os.Stat(p); err != nil {
|
| 128 |
+
t.Fatalf("path %s does not exist: %v", p, err)
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
func TestGetLdmapWithFakeOutput(t *testing.T) {
|
| 134 |
+
// fake ldconfig output with a single mapping
|
| 135 |
+
original := ldconfigRunner
|
| 136 |
+
defer func() { ldconfigRunner = original }()
|
| 137 |
+
|
| 138 |
+
// create a fake file on disk to satisfy os.Stat checks in getLdmap
|
| 139 |
+
tmpDir := t.TempDir()
|
| 140 |
+
tmp := filepath.Join(tmpDir, "libfake.so")
|
| 141 |
+
f, err := os.Create(tmp)
|
| 142 |
+
if err != nil {
|
| 143 |
+
t.Fatalf("failed to create tmp file: %v", err)
|
| 144 |
+
}
|
| 145 |
+
f.Close()
|
| 146 |
+
|
| 147 |
+
// Because getLdmap checks the path exists, return tmp in the fake output
|
| 148 |
+
ldconfigRunner = func() ([]byte, error) {
|
| 149 |
+
return []byte("libfake.so (libc6,x86-64) => " + tmp + "\n"), nil
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
m := getLdmap()
|
| 153 |
+
if got, ok := m["libfake.so"]; !ok {
|
| 154 |
+
t.Fatalf("expected libfake.so in map")
|
| 155 |
+
} else if got != tmp {
|
| 156 |
+
t.Fatalf("expected path %s, got %s", tmp, got)
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
func TestResolveSonamesUsesLdmapFallback(t *testing.T) {
|
| 161 |
+
original := ldconfigRunner
|
| 162 |
+
defer func() { ldconfigRunner = original }()
|
| 163 |
+
|
| 164 |
+
tmpDir := t.TempDir()
|
| 165 |
+
tmp := filepath.Join(tmpDir, "libfake2.so")
|
| 166 |
+
f, err := os.Create(tmp)
|
| 167 |
+
if err != nil {
|
| 168 |
+
t.Fatalf("failed to create tmp file: %v", err)
|
| 169 |
+
}
|
| 170 |
+
f.Close()
|
| 171 |
+
|
| 172 |
+
ldconfigRunner = func() ([]byte, error) {
|
| 173 |
+
return []byte("libfake2.so (libc6,x86-64) => " + tmp + "\n"), nil
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
// needed contains a soname that won't be found in rpaths or std dirs
|
| 177 |
+
rpaths := normalizeRpaths([]string{}, tmpDir)
|
| 178 |
+
out := resolveSonames([]string{"libfake2.so"}, rpaths)
|
| 179 |
+
if len(out) != 1 {
|
| 180 |
+
t.Fatalf("expected 1 resolved path, got %d", len(out))
|
| 181 |
+
}
|
| 182 |
+
if out[0] != tmp {
|
| 183 |
+
t.Fatalf("expected %s, got %s", tmp, out[0])
|
| 184 |
+
}
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
func TestResolveSonamesOriginExpansion(t *testing.T) {
|
| 188 |
+
// Create a temp dir and a lib subdir to simulate $ORIGIN/lib
|
| 189 |
+
tmpDir := t.TempDir()
|
| 190 |
+
libDir := filepath.Join(tmpDir, "lib")
|
| 191 |
+
if err := os.Mkdir(libDir, 0755); err != nil {
|
| 192 |
+
t.Fatalf("failed create lib dir: %v", err)
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
libName := "liborigin.so"
|
| 196 |
+
libPath := filepath.Join(libDir, libName)
|
| 197 |
+
f, err := os.Create(libPath)
|
| 198 |
+
if err != nil {
|
| 199 |
+
t.Fatalf("failed to create lib file: %v", err)
|
| 200 |
+
}
|
| 201 |
+
f.Close()
|
| 202 |
+
|
| 203 |
+
// rpath using $ORIGIN should resolve to tmpDir/lib
|
| 204 |
+
rpaths1 := normalizeRpaths([]string{"$ORIGIN/lib"}, tmpDir)
|
| 205 |
+
out := resolveSonames([]string{libName}, rpaths1)
|
| 206 |
+
if len(out) != 1 {
|
| 207 |
+
t.Fatalf("expected 1 resolved path for $ORIGIN, got %d", len(out))
|
| 208 |
+
}
|
| 209 |
+
if out[0] != libPath {
|
| 210 |
+
t.Fatalf("expected %s, got %s", libPath, out[0])
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
// relative rpath should also resolve against origin
|
| 214 |
+
rpaths2 := normalizeRpaths([]string{"lib"}, tmpDir)
|
| 215 |
+
out2 := resolveSonames([]string{libName}, rpaths2)
|
| 216 |
+
if len(out2) != 1 {
|
| 217 |
+
t.Fatalf("expected 1 resolved path for relative rpath, got %d", len(out2))
|
| 218 |
+
}
|
| 219 |
+
if out2[0] != libPath {
|
| 220 |
+
t.Fatalf("expected %s, got %s", libPath, out2[0])
|
| 221 |
+
}
|
| 222 |
+
}
|
landrun-main/internal/elfdeps/testdata/liba.c
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
int a() { return 1; }
|
landrun-main/internal/elfdeps/testdata/libb.c
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
int a();
|
| 2 |
+
int b() { return a(); }
|
landrun-main/internal/elfdeps/testdata/main.c
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
int b();
|
| 2 |
+
int main() { b(); return 0; }
|
landrun-main/internal/exec/runner.go
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package exec
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"os/exec"
|
| 5 |
+
"syscall"
|
| 6 |
+
|
| 7 |
+
"github.com/zouuup/landrun/internal/log"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
func Run(args []string, env []string) error {
|
| 11 |
+
binary, err := exec.LookPath(args[0])
|
| 12 |
+
if err != nil {
|
| 13 |
+
return err
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
log.Info("Executing: %v", args)
|
| 17 |
+
|
| 18 |
+
// Only pass the explicitly specified environment variables
|
| 19 |
+
// If env is empty, no environment variables will be passed
|
| 20 |
+
return syscall.Exec(binary, args, env)
|
| 21 |
+
}
|
landrun-main/internal/log/log.go
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package log
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"log"
|
| 5 |
+
"os"
|
| 6 |
+
"strings"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
type Level int
|
| 10 |
+
|
| 11 |
+
const (
|
| 12 |
+
LevelError Level = iota
|
| 13 |
+
LevelInfo
|
| 14 |
+
LevelDebug
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
var (
|
| 18 |
+
debug = log.New(os.Stderr, "[landrun:debug] ", log.LstdFlags)
|
| 19 |
+
info = log.New(os.Stderr, "[landrun] ", log.LstdFlags)
|
| 20 |
+
error = log.New(os.Stderr, "[landrun:error] ", log.LstdFlags)
|
| 21 |
+
|
| 22 |
+
currentLevel = LevelInfo // default level
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
// SetLevel sets the logging level
|
| 26 |
+
func SetLevel(level string) {
|
| 27 |
+
switch strings.ToLower(level) {
|
| 28 |
+
case "error":
|
| 29 |
+
currentLevel = LevelError
|
| 30 |
+
case "info":
|
| 31 |
+
currentLevel = LevelInfo
|
| 32 |
+
case "debug":
|
| 33 |
+
currentLevel = LevelDebug
|
| 34 |
+
default:
|
| 35 |
+
currentLevel = LevelError
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// Debug logs a debug message
|
| 40 |
+
func Debug(format string, v ...interface{}) {
|
| 41 |
+
if currentLevel >= LevelDebug {
|
| 42 |
+
debug.Printf(format, v...)
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// Info logs an info message
|
| 47 |
+
func Info(format string, v ...interface{}) {
|
| 48 |
+
if currentLevel >= LevelInfo {
|
| 49 |
+
info.Printf(format, v...)
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// Error logs an error message
|
| 54 |
+
func Error(format string, v ...interface{}) {
|
| 55 |
+
if currentLevel >= LevelError {
|
| 56 |
+
error.Printf(format, v...)
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// Fatal logs an error message and exits
|
| 61 |
+
func Fatal(format string, v ...interface{}) {
|
| 62 |
+
error.Printf(format, v...)
|
| 63 |
+
os.Exit(1)
|
| 64 |
+
}
|
landrun-main/internal/sandbox/sandbox.go
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package sandbox
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"os"
|
| 6 |
+
|
| 7 |
+
"github.com/landlock-lsm/go-landlock/landlock"
|
| 8 |
+
"github.com/landlock-lsm/go-landlock/landlock/syscall"
|
| 9 |
+
"github.com/zouuup/landrun/internal/log"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
type Config struct {
|
| 13 |
+
ReadOnlyPaths []string
|
| 14 |
+
ReadWritePaths []string
|
| 15 |
+
ReadOnlyExecutablePaths []string
|
| 16 |
+
ReadWriteExecutablePaths []string
|
| 17 |
+
BindTCPPorts []int
|
| 18 |
+
ConnectTCPPorts []int
|
| 19 |
+
BestEffort bool
|
| 20 |
+
UnrestrictedFilesystem bool
|
| 21 |
+
UnrestrictedNetwork bool
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
// getReadWriteExecutableRights returns a full set of permissions including execution
|
| 25 |
+
func getReadWriteExecutableRights(dir bool) landlock.AccessFSSet {
|
| 26 |
+
accessRights := landlock.AccessFSSet(0)
|
| 27 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSExecute)
|
| 28 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSReadFile)
|
| 29 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSWriteFile)
|
| 30 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSTruncate)
|
| 31 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSIoctlDev)
|
| 32 |
+
|
| 33 |
+
if dir {
|
| 34 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSReadDir)
|
| 35 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSRemoveDir)
|
| 36 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSRemoveFile)
|
| 37 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeChar)
|
| 38 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeDir)
|
| 39 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeReg)
|
| 40 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeSock)
|
| 41 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeFifo)
|
| 42 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeBlock)
|
| 43 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeSym)
|
| 44 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSRefer)
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
return accessRights
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
func getReadOnlyExecutableRights(dir bool) landlock.AccessFSSet {
|
| 51 |
+
accessRights := landlock.AccessFSSet(0)
|
| 52 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSExecute)
|
| 53 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSReadFile)
|
| 54 |
+
if dir {
|
| 55 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSReadDir)
|
| 56 |
+
}
|
| 57 |
+
return accessRights
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// getReadOnlyRights returns permissions for read-only access
|
| 61 |
+
func getReadOnlyRights(dir bool) landlock.AccessFSSet {
|
| 62 |
+
accessRights := landlock.AccessFSSet(0)
|
| 63 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSReadFile)
|
| 64 |
+
if dir {
|
| 65 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSReadDir)
|
| 66 |
+
}
|
| 67 |
+
return accessRights
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// getReadWriteRights returns permissions for read-write access
|
| 71 |
+
func getReadWriteRights(dir bool) landlock.AccessFSSet {
|
| 72 |
+
accessRights := landlock.AccessFSSet(0)
|
| 73 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSReadFile)
|
| 74 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSWriteFile)
|
| 75 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSTruncate)
|
| 76 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSIoctlDev)
|
| 77 |
+
if dir {
|
| 78 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSReadDir)
|
| 79 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSRemoveDir)
|
| 80 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSRemoveFile)
|
| 81 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeChar)
|
| 82 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeDir)
|
| 83 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeReg)
|
| 84 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeSock)
|
| 85 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeFifo)
|
| 86 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeBlock)
|
| 87 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSMakeSym)
|
| 88 |
+
accessRights |= landlock.AccessFSSet(syscall.AccessFSRefer)
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
return accessRights
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// isDirectory checks if the given path is a directory
|
| 95 |
+
func isDirectory(path string) bool {
|
| 96 |
+
fileInfo, err := os.Stat(path)
|
| 97 |
+
if err != nil {
|
| 98 |
+
return false
|
| 99 |
+
}
|
| 100 |
+
return fileInfo.IsDir()
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
func Apply(cfg Config) error {
|
| 104 |
+
log.Info("Sandbox config: %+v", cfg)
|
| 105 |
+
|
| 106 |
+
// Get the most advanced Landlock version available
|
| 107 |
+
llCfg := landlock.V5
|
| 108 |
+
if cfg.BestEffort {
|
| 109 |
+
llCfg = llCfg.BestEffort()
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// Collect our rules
|
| 113 |
+
var file_rules []landlock.Rule
|
| 114 |
+
var net_rules []landlock.Rule
|
| 115 |
+
|
| 116 |
+
// Process executable paths
|
| 117 |
+
for _, path := range cfg.ReadOnlyExecutablePaths {
|
| 118 |
+
log.Debug("Adding read-only executable path: %s", path)
|
| 119 |
+
file_rules = append(file_rules, landlock.PathAccess(getReadOnlyExecutableRights(isDirectory(path)), path))
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
for _, path := range cfg.ReadWriteExecutablePaths {
|
| 123 |
+
log.Debug("Adding read-write executable path: %s", path)
|
| 124 |
+
file_rules = append(file_rules, landlock.PathAccess(getReadWriteExecutableRights(isDirectory(path)), path))
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
// Process read-only paths
|
| 128 |
+
for _, path := range cfg.ReadOnlyPaths {
|
| 129 |
+
log.Debug("Adding read-only path: %s", path)
|
| 130 |
+
file_rules = append(file_rules, landlock.PathAccess(getReadOnlyRights(isDirectory(path)), path))
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
// Process read-write paths
|
| 134 |
+
for _, path := range cfg.ReadWritePaths {
|
| 135 |
+
log.Debug("Adding read-write path: %s", path)
|
| 136 |
+
file_rules = append(file_rules, landlock.PathAccess(getReadWriteRights(isDirectory(path)), path))
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
// Add rules for TCP port binding
|
| 140 |
+
for _, port := range cfg.BindTCPPorts {
|
| 141 |
+
log.Debug("Adding TCP bind port: %d", port)
|
| 142 |
+
net_rules = append(net_rules, landlock.BindTCP(uint16(port)))
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
// Add rules for TCP connections
|
| 146 |
+
for _, port := range cfg.ConnectTCPPorts {
|
| 147 |
+
log.Debug("Adding TCP connect port: %d", port)
|
| 148 |
+
net_rules = append(net_rules, landlock.ConnectTCP(uint16(port)))
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
if cfg.UnrestrictedFilesystem && cfg.UnrestrictedNetwork {
|
| 152 |
+
log.Info("Unrestricted filesystem and network access enabled; no rules applied.")
|
| 153 |
+
return nil
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
if cfg.UnrestrictedFilesystem {
|
| 157 |
+
log.Info("Unrestricted filesystem access enabled.")
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
if cfg.UnrestrictedNetwork {
|
| 161 |
+
log.Info("Unrestricted network access enabled")
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
// If we have no rules, just return
|
| 165 |
+
if len(file_rules) == 0 && len(net_rules) == 0 && !cfg.UnrestrictedFilesystem && !cfg.UnrestrictedNetwork {
|
| 166 |
+
log.Error("No rules provided, applying default restrictive rules, this will restrict anything landlock can do.")
|
| 167 |
+
err := llCfg.Restrict()
|
| 168 |
+
if err != nil {
|
| 169 |
+
return fmt.Errorf("failed to apply default Landlock restrictions: %w", err)
|
| 170 |
+
}
|
| 171 |
+
log.Info("Default restrictive Landlock rules applied successfully")
|
| 172 |
+
return nil
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
// Apply all rules at once
|
| 176 |
+
log.Debug("Applying Landlock restrictions")
|
| 177 |
+
if !cfg.UnrestrictedFilesystem {
|
| 178 |
+
err := llCfg.RestrictPaths(file_rules...)
|
| 179 |
+
if err != nil {
|
| 180 |
+
return fmt.Errorf("failed to apply Landlock filesystem restrictions: %w", err)
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
if !cfg.UnrestrictedNetwork {
|
| 184 |
+
err := llCfg.RestrictNet(net_rules...)
|
| 185 |
+
if err != nil {
|
| 186 |
+
return fmt.Errorf("failed to apply Landlock network restrictions: %w", err)
|
| 187 |
+
}
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
log.Info("Landlock restrictions applied successfully")
|
| 191 |
+
return nil
|
| 192 |
+
}
|
landrun-main/test.sh
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Check if we should keep the binary
|
| 4 |
+
KEEP_BINARY=false
|
| 5 |
+
USE_SYSTEM_BINARY=false
|
| 6 |
+
NO_BUILD=false
|
| 7 |
+
# Whether we should try to do network calls during testing.
|
| 8 |
+
INTERNET_ACCESS=true
|
| 9 |
+
|
| 10 |
+
while [ "$#" -gt 0 ]; do
|
| 11 |
+
case "$1" in
|
| 12 |
+
"--keep-binary")
|
| 13 |
+
KEEP_BINARY=true
|
| 14 |
+
shift
|
| 15 |
+
;;
|
| 16 |
+
"--use-system")
|
| 17 |
+
USE_SYSTEM_BINARY=true
|
| 18 |
+
shift
|
| 19 |
+
;;
|
| 20 |
+
"--no-build")
|
| 21 |
+
NO_BUILD=true
|
| 22 |
+
shift
|
| 23 |
+
;;
|
| 24 |
+
"--offline")
|
| 25 |
+
INTERNET_ACCESS=false
|
| 26 |
+
shift
|
| 27 |
+
;;
|
| 28 |
+
*)
|
| 29 |
+
echo "Unknown parameter: $1"
|
| 30 |
+
exit 1
|
| 31 |
+
;;
|
| 32 |
+
esac
|
| 33 |
+
done
|
| 34 |
+
|
| 35 |
+
# Don't exit on error, we'll handle errors in the run_test function
|
| 36 |
+
set +e
|
| 37 |
+
|
| 38 |
+
# Colors for output
|
| 39 |
+
RED='\033[0;31m'
|
| 40 |
+
GREEN='\033[0;32m'
|
| 41 |
+
YELLOW='\033[1;33m'
|
| 42 |
+
NC='\033[0m' # No Color
|
| 43 |
+
|
| 44 |
+
# Function to print colored output
|
| 45 |
+
print_status() {
|
| 46 |
+
echo -e "${YELLOW}[TEST]${NC} $1"
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
print_success() {
|
| 50 |
+
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
print_error() {
|
| 54 |
+
echo -e "${RED}[ERROR]${NC} $1"
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
# Build the binary if not using system binary
|
| 58 |
+
if [ "$USE_SYSTEM_BINARY" = false ]; then
|
| 59 |
+
if [ "$NO_BUILD" = false ]; then
|
| 60 |
+
print_status "Building landrun binary..."
|
| 61 |
+
go build -o landrun cmd/landrun/main.go
|
| 62 |
+
if [ $? -ne 0 ]; then
|
| 63 |
+
print_error "Failed to build landrun binary"
|
| 64 |
+
exit 1
|
| 65 |
+
fi
|
| 66 |
+
print_success "Binary built successfully"
|
| 67 |
+
else
|
| 68 |
+
print_success "Using already built landrun binary"
|
| 69 |
+
fi
|
| 70 |
+
fi
|
| 71 |
+
|
| 72 |
+
# Create test directories
|
| 73 |
+
TEST_DIR="test_env"
|
| 74 |
+
RO_DIR="$TEST_DIR/ro"
|
| 75 |
+
RO_DIR_NESTED_RO="$RO_DIR/ro_nested_ro_1"
|
| 76 |
+
RO_DIR_NESTED_RW="$RO_DIR/ro_nested_rw_1"
|
| 77 |
+
RO_DIR_NESTED_EXEC="$RO_DIR/ro_nested_exec"
|
| 78 |
+
|
| 79 |
+
RW_DIR="$TEST_DIR/rw"
|
| 80 |
+
RW_DIR_NESTED_RO="$RW_DIR/rw_nested_ro_1"
|
| 81 |
+
RW_DIR_NESTED_RW="$RW_DIR/rw_nested_rw_1"
|
| 82 |
+
RW_DIR_NESTED_EXEC="$RW_DIR/rw_nested_exec"
|
| 83 |
+
|
| 84 |
+
EXEC_DIR="$TEST_DIR/exec"
|
| 85 |
+
NESTED_DIR="$TEST_DIR/nested/path/deep"
|
| 86 |
+
|
| 87 |
+
print_status "Setting up test environment..."
|
| 88 |
+
rm -rf "$TEST_DIR"
|
| 89 |
+
mkdir -p "$RO_DIR" "$RW_DIR" "$EXEC_DIR" "$NESTED_DIR" "$RO_DIR_NESTED_RO" "$RO_DIR_NESTED_RW" "$RO_DIR_NESTED_EXEC" "$RW_DIR_NESTED_RO" "$RW_DIR_NESTED_RW" "$RW_DIR_NESTED_EXEC"
|
| 90 |
+
|
| 91 |
+
# Create test files
|
| 92 |
+
echo "readonly content" > "$RO_DIR/test.txt"
|
| 93 |
+
echo "readwrite content" > "$RW_DIR/test.txt"
|
| 94 |
+
echo "nested content" > "$NESTED_DIR/test.txt"
|
| 95 |
+
echo "#!/bin/bash" > "$EXEC_DIR/test.sh"
|
| 96 |
+
echo "echo 'executable content'" >> "$EXEC_DIR/test.sh"
|
| 97 |
+
chmod +x "$EXEC_DIR/test.sh"
|
| 98 |
+
cp $EXEC_DIR/test.sh $EXEC_DIR/test2.sh
|
| 99 |
+
|
| 100 |
+
cp "$RO_DIR/test.txt" "$RO_DIR_NESTED_RO/test.txt"
|
| 101 |
+
cp "$RO_DIR/test.txt" "$RW_DIR_NESTED_RO/test.txt"
|
| 102 |
+
|
| 103 |
+
cp "$RW_DIR/test.txt" "$RO_DIR_NESTED_RW/test.txt"
|
| 104 |
+
cp "$RW_DIR/test.txt" "$RW_DIR_NESTED_RW/test.txt"
|
| 105 |
+
|
| 106 |
+
cp "$EXEC_DIR/test.sh" "$RO_DIR_NESTED_EXEC/test.sh"
|
| 107 |
+
cp "$EXEC_DIR/test.sh" "$RW_DIR_NESTED_EXEC/test.sh"
|
| 108 |
+
cp "$EXEC_DIR/test.sh" "$RO_DIR_NESTED_RO/test.sh"
|
| 109 |
+
cp "$EXEC_DIR/test.sh" "$RW_DIR_NESTED_RO/test.sh"
|
| 110 |
+
cp "$EXEC_DIR/test.sh" "$RO_DIR_NESTED_RW/test.sh"
|
| 111 |
+
cp "$EXEC_DIR/test.sh" "$RW_DIR_NESTED_RW/test.sh"
|
| 112 |
+
|
| 113 |
+
# Create a script in RW dir to test execution in RW dirs
|
| 114 |
+
echo "#!/bin/bash" > "$RW_DIR/rw_script.sh"
|
| 115 |
+
echo "echo 'this script is in a read-write directory'" >> "$RW_DIR/rw_script.sh"
|
| 116 |
+
chmod +x "$RW_DIR/rw_script.sh"
|
| 117 |
+
|
| 118 |
+
# Function to run a test case
|
| 119 |
+
run_test() {
|
| 120 |
+
local name="$1"
|
| 121 |
+
local cmd="$2"
|
| 122 |
+
local expected_exit="$3"
|
| 123 |
+
|
| 124 |
+
# Replace ./landrun with landrun if using system binary
|
| 125 |
+
if [ "$USE_SYSTEM_BINARY" = true ]; then
|
| 126 |
+
cmd="${cmd//.\/landrun/landrun}"
|
| 127 |
+
fi
|
| 128 |
+
|
| 129 |
+
print_status "Running test: $name"
|
| 130 |
+
eval "$cmd"
|
| 131 |
+
local exit_code=$?
|
| 132 |
+
|
| 133 |
+
if [ $exit_code -eq $expected_exit ]; then
|
| 134 |
+
print_success "Test passed: $name"
|
| 135 |
+
return 0
|
| 136 |
+
else
|
| 137 |
+
print_error "Test failed: $name (expected exit $expected_exit, got $exit_code)"
|
| 138 |
+
exit 1
|
| 139 |
+
fi
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
# Test cases
|
| 143 |
+
print_status "Starting test cases..."
|
| 144 |
+
|
| 145 |
+
# Basic access tests
|
| 146 |
+
run_test "Read-only access to file" \
|
| 147 |
+
"./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $RO_DIR -- cat $RO_DIR/test.txt" \
|
| 148 |
+
0
|
| 149 |
+
|
| 150 |
+
run_test "Read-only access to nested file" \
|
| 151 |
+
"./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $RO_DIR -- cat $RO_DIR_NESTED_RO/test.txt" \
|
| 152 |
+
0
|
| 153 |
+
|
| 154 |
+
run_test "Write access to nested directory writable nested in read-only directory" \
|
| 155 |
+
"./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $RO_DIR --rw $RO_DIR_NESTED_RW -- touch $RO_DIR_NESTED_RW/created_file" \
|
| 156 |
+
0
|
| 157 |
+
|
| 158 |
+
run_test "Write access to nested file writable nested in read-only directory" \
|
| 159 |
+
"./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $RO_DIR --rw $RO_DIR_NESTED_RW/created_file -- touch $RO_DIR_NESTED_RW/created_file" \
|
| 160 |
+
0
|
| 161 |
+
|
| 162 |
+
run_test "Read-write access to file" \
|
| 163 |
+
"./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $RO_DIR --rw $RW_DIR touch $RW_DIR/new.txt" \
|
| 164 |
+
0
|
| 165 |
+
|
| 166 |
+
run_test "No write access to read-only directory" \
|
| 167 |
+
"./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $RO_DIR --rw $RW_DIR touch $RO_DIR/new.txt" \
|
| 168 |
+
1
|
| 169 |
+
|
| 170 |
+
# Executable permission tests
|
| 171 |
+
run_test "Execute access with rox flag" \
|
| 172 |
+
"./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --rox $EXEC_DIR -- $EXEC_DIR/test.sh" \
|
| 173 |
+
0
|
| 174 |
+
|
| 175 |
+
run_test "Execute access with rox flag on file" \
|
| 176 |
+
"./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --rox $EXEC_DIR/test.sh -- $EXEC_DIR/test.sh" \
|
| 177 |
+
0
|
| 178 |
+
|
| 179 |
+
run_test "Execute access with rox flag on a file that is executable in same directory that one is allowed" \
|
| 180 |
+
"./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --rox $EXEC_DIR/test.sh -- $EXEC_DIR/test2.sh" \
|
| 181 |
+
1
|
| 182 |
+
|
| 183 |
+
run_test "Execute a file with --add-exec flag" \
|
| 184 |
+
"./landrun --log-level debug --add-exec --rox /usr --ro /lib --ro /lib64 --rox $EXEC_DIR/test.sh -- $EXEC_DIR/test2.sh" \
|
| 185 |
+
0
|
| 186 |
+
|
| 187 |
+
run_test "Execute a file with --add-exec and --ldd flag" \
|
| 188 |
+
"./landrun --log-level debug --add-exec --ldd -- $(which true)" \
|
| 189 |
+
0
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
run_test "No execute access with just ro flag" \
|
| 193 |
+
"./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $EXEC_DIR -- $EXEC_DIR/test.sh" \
|
| 194 |
+
1
|
| 195 |
+
|
| 196 |
+
run_test "Execute access in read-write directory" \
|
| 197 |
+
"./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --rwx $RW_DIR -- $RW_DIR/rw_script.sh" \
|
| 198 |
+
0
|
| 199 |
+
|
| 200 |
+
run_test "No execute access in read-write directory without rwx" \
|
| 201 |
+
"./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --rw $RW_DIR -- $RW_DIR/rw_script.sh" \
|
| 202 |
+
1
|
| 203 |
+
|
| 204 |
+
# Directory traversal tests
|
| 205 |
+
run_test "Directory traversal with root access" \
|
| 206 |
+
"./landrun --log-level debug --rox / -- ls /usr" \
|
| 207 |
+
0
|
| 208 |
+
|
| 209 |
+
run_test "Deep directory traversal" \
|
| 210 |
+
"./landrun --log-level debug --rox / -- ls $NESTED_DIR" \
|
| 211 |
+
0
|
| 212 |
+
|
| 213 |
+
# Multiple paths and complex specifications
|
| 214 |
+
run_test "Multiple read paths" \
|
| 215 |
+
"./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --ro $RO_DIR --ro $NESTED_DIR -- cat $NESTED_DIR/test.txt" \
|
| 216 |
+
0
|
| 217 |
+
|
| 218 |
+
run_test "Comma-separated paths" \
|
| 219 |
+
"./landrun --log-level debug --rox /usr --ro /lib,/lib64,$RO_DIR -- cat $RO_DIR/test.txt" \
|
| 220 |
+
0
|
| 221 |
+
|
| 222 |
+
# System command tests
|
| 223 |
+
run_test "Simple system command" \
|
| 224 |
+
"./landrun --log-level debug --rox /usr --ro /etc -- whoami" \
|
| 225 |
+
0
|
| 226 |
+
|
| 227 |
+
run_test "System command with arguments" \
|
| 228 |
+
"./landrun --log-level debug --rox / -- ls -la /usr/bin" \
|
| 229 |
+
0
|
| 230 |
+
|
| 231 |
+
# Edge cases
|
| 232 |
+
run_test "Non-existent read-only path" \
|
| 233 |
+
"./landrun --log-level debug --ro /usr --ro /lib --ro /lib64 --ro /nonexistent/path -- ls" \
|
| 234 |
+
1
|
| 235 |
+
|
| 236 |
+
run_test "No configuration" \
|
| 237 |
+
"./landrun --log-level debug -- ls /" \
|
| 238 |
+
1
|
| 239 |
+
|
| 240 |
+
# Process creation and redirection tests
|
| 241 |
+
run_test "Process creation with pipe" \
|
| 242 |
+
"./landrun --log-level debug --rox / --env PATH -- bash -c 'ls /usr | grep bin'" \
|
| 243 |
+
0
|
| 244 |
+
|
| 245 |
+
run_test "File redirection" \
|
| 246 |
+
"./landrun --log-level debug --rox / --rw $RW_DIR --env PATH -- bash -c 'ls /usr > $RW_DIR/output.txt && cat $RW_DIR/output.txt'" \
|
| 247 |
+
0
|
| 248 |
+
|
| 249 |
+
# Network restrictions tests (if kernel supports it)
|
| 250 |
+
$INTERNET_ACCESS && run_test "TCP connection without permission" \
|
| 251 |
+
"./landrun --log-level debug --rox /usr --ro / -- curl -s --connect-timeout 2 https://example.com" \
|
| 252 |
+
7
|
| 253 |
+
|
| 254 |
+
$INTERNET_ACCESS && run_test "TCP connection with permission" \
|
| 255 |
+
"./landrun --log-level debug --rox /usr --ro / --connect-tcp 443 -- curl -s --connect-timeout 2 https://example.com" \
|
| 256 |
+
0
|
| 257 |
+
|
| 258 |
+
# Environment isolation tests
|
| 259 |
+
export TEST_ENV_VAR="test_value_123"
|
| 260 |
+
run_test "Environment isolation" \
|
| 261 |
+
"./landrun --log-level debug --rox /usr --ro / -- bash -c 'echo \$TEST_ENV_VAR'" \
|
| 262 |
+
0
|
| 263 |
+
|
| 264 |
+
run_test "Environment isolation (no variables should be passed)" \
|
| 265 |
+
"./landrun --log-level debug --rox /usr --ro / -- bash -c '[[ -z \$TEST_ENV_VAR ]] && echo \"No env var\" || echo \$TEST_ENV_VAR'" \
|
| 266 |
+
0
|
| 267 |
+
|
| 268 |
+
run_test "Passing specific environment variable" \
|
| 269 |
+
"./landrun --log-level debug --rox /usr --ro / --env TEST_ENV_VAR --env PATH -- bash -c 'echo \$TEST_ENV_VAR | grep \"test_value_123\"'" \
|
| 270 |
+
0
|
| 271 |
+
|
| 272 |
+
run_test "Passing custom environment variable" \
|
| 273 |
+
"./landrun --log-level debug --rox /usr --ro / --env CUSTOM_VAR=custom_value --env PATH -- bash -c 'echo \$CUSTOM_VAR | grep \"custom_value\"'" \
|
| 274 |
+
0
|
| 275 |
+
|
| 276 |
+
# Combining different permission types
|
| 277 |
+
run_test "Mixed permissions" \
|
| 278 |
+
"./landrun --log-level debug --rox /usr --ro /lib --ro /lib64 --rox $EXEC_DIR --rwx $RW_DIR --env PATH -- bash -c '$EXEC_DIR/test.sh > $RW_DIR/output.txt && cat $RW_DIR/output.txt'" \
|
| 279 |
+
0
|
| 280 |
+
|
| 281 |
+
# Specific regression tests for bugs we fixed
|
| 282 |
+
run_test "Root path traversal regression test" \
|
| 283 |
+
"./landrun --log-level debug --rox /usr -- $(which ls) /usr" \
|
| 284 |
+
0
|
| 285 |
+
|
| 286 |
+
run_test "Execute from read-only paths regression test" \
|
| 287 |
+
"./landrun --log-level debug --rox /usr --ro /usr/bin -- $(which id)" \
|
| 288 |
+
0
|
| 289 |
+
|
| 290 |
+
run_test "Unrestricted filesystem access" \
|
| 291 |
+
"./landrun --log-level debug --unrestricted-filesystem ls /usr" \
|
| 292 |
+
0
|
| 293 |
+
|
| 294 |
+
$INTERNET_ACCESS && run_test "Unrestricted network access" \
|
| 295 |
+
"./landrun --log-level debug --unrestricted-network --rox /usr --ro /etc -- curl -s --connect-timeout 2 https://example.com" \
|
| 296 |
+
0
|
| 297 |
+
|
| 298 |
+
run_test "Restricted filesystem access" \
|
| 299 |
+
"./landrun --log-level debug ls /usr" \
|
| 300 |
+
1
|
| 301 |
+
|
| 302 |
+
$INTERNET_ACCESS && run_test "Restricted network access" \
|
| 303 |
+
"./landrun --log-level debug --rox /usr --ro /etc -- curl -s --connect-timeout 2 https://example.com" \
|
| 304 |
+
7
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
# Cleanup
|
| 308 |
+
print_status "Cleaning up..."
|
| 309 |
+
rm -rf "$TEST_DIR"
|
| 310 |
+
if [ "$KEEP_BINARY" = false ] && [ "$USE_SYSTEM_BINARY" = false ]; then
|
| 311 |
+
rm -f landrun
|
| 312 |
+
fi
|
| 313 |
+
|
| 314 |
+
print_success "All tests completed!"
|