@tlhunter + @bengl
GitHub: IntrinsicLabs/osgood

Introducing


Thomas Hunter II + Bryan English

Topics

  1. Introduction
  2. Why use Osgood?
  3. Building Apps
  4. Osgood Internals
  5. The Future of Osgood

Introduction

Who are We?

  • Thomas and Bryan work at Intrinsic
  • We built a robust Node.js security sandbox
  • Threat Model: Attackers running arbitrary code
    • Such as malicious modules from npm
  • Prevents attackers from exfiltrating data
  • Security Policies and sandboxing per-route

Osgood Background

  • We spent years on this Node.js product
  • Asked how it would look without legacy constraints
  • Osgood started as an experiment
  • Built with assumptions:
    • Most apps are HTTP servers with routing
    • Shared global state isn't that beneficial

What is Osgood?

  • A platform for running Server Side JavaScript
  • Currently only runs as an HTTP Server
  • Emphasis on security and small I/O API surface
  • Uses V8 to provide modern JavaScript features
  • Distributed as a single precompiled static binary
  • Open Source under the MIT License
About 2/3 Rust, 1/3 JavaScript, and some C++

Osgood Philosophy

  • Don't be a general-purpose tool (à la Node.js)
  • All I/O must be configurable via policies
  • Hide network access behind high-level APIs
    • E.g. userland cannot generate a TCP packet
  • Apps declare policies before user code runs
    • I/O that isn't explicitly whitelisted will fail
  • Mimic existing APIs instead of building new ones

Why Use Osgood?

It's Fast

Simple ‘Hello World’ with equal-sized payloads

$ wrk -c 100 -d 60 http://localhost:3000/hello

Node.js v12.4

Requests/sec:  13,010

Osgood

Requests/sec:  43,741 ~3.3x

Less Boilerplate

// $ node server.js
const http = require('http');

const server = http.createServer((req, res) => {
  if (req.url === '/hello') {
    res.end('Hello, world!');
  } else {
    res.statusCode = 404;
    res.end('Not Found');
  }
});
server.listen(3000);

Less Boilerplate

// $ osgood app.js
app.port = 3000;
app.get('/hello', 'worker.js');

// worker.js
export default () => "Hello, World!";






Intuitive API

  • Symmetric: Request comes in, Response goes out
// worker.js
export default function(req) {
  const requestBody = await req.json();
  const obj = {
    echo: requestBody
  };
  const headers = new Headers({
    'Content-Type': 'application/json'
  });
  const body = JSON.stringify(obj);
  return new Response(body, { headers, status });
}

Building Apps

Basic App Concepts

  • Osgood apps use a single Application file
    • This file handles global configuration
  • The App file associates routes with Worker files
// app.js
app.interface = '127.0.0.1';
app.port = 8080;
app.get('/merge/:user', 'merge.js');
  • Later this file is executed via the osgood binary
$ osgood app.js

Osgood Performs Routing

  • Osgood provides the HTTP server
  • Because of this it's able to perform routing
  • Think Node.js + Express or Apache + .htaccess + PHP
  • This allows Osgood to isolate JavaScript contexts
// app.js
app.get('/users',           'list-users.js');
app.get('/users/:id',       'view-user.js');
app.delete('/users/:id',    'delete-user.js');
app.post('/users',          'create-user.js');
app.put('/users/:id',       'update-user.js');

Osgood Worker Files

  • Export a default request handler
export default async function (request, context) {
  const u = context.params.user;
  const [gists_req, repos_req] = await Promise.all([
    fetch(`https://api.github.com/users/${u}/gists`),
    fetch(`https://api.github.com/users/${u}/repos`),
  ]);
  const [gists, repos] = await Promise.all([
    gists_req.json(),
    repos_req.json(),
  ]);
  return { gists, repos };
}

Mandatory Whitelists

  • I/O (such as outbound HTTP) must be whitelisted
  • Any non-whitelisted operation will fail
  • Imagine how easy those GDPR audits will be
// app.js
const GISTS = 'https://api.github.com/users/*/gists';
const REPOS = 'https://api.github.com/users/*/repos';

app.get('/merge/:user', 'merge.js', policy => {
  policy.outboundHttp.allowGet(GISTS);
  policy.outboundHttp.allowGet(REPOS);
});

JavaScript Featues

  • Osgood provides many common browser APIs
    • fetch(), Request, Response, Headers
    • URL, URLSearchParams
    • ReadableStream, WritableStream
  • V8 provides bleeding-edge JavaScript features
    • BigInt
    • class, private fields
    • async, await, import, export
    • RegExp named capture groups

Gotchas

  • Each worker runs in a separate thread
  • Global state cannot be shared between workers
    • In-process Redis with localStorage interface?
  • Files cannot be dynamically loaded with import
    • Does not come with require()
  • Code currently cannot access filesystem
    • However, there is a syntax for static asset routes

Osgood Internals

V8

V8

Rust

Rust

Rust Is *Super Cool*

  • Memory Safety
  • Ownership
  • Traits
  • Promises Futures
    • async/await ??????

Tokio

Tokio

Tokio

It's like libuv

Except you get threads.

Hyper

Hyper

osgood-v8

V8

+

bindgen

(unsafe)

+

our wrapper code

(safe (and simple!))

Exposing Native Functions to JavaScript

// C++

static void
MyFunction(const FunctionCallbackInfo<Value>& args)
{
  Isolate * isolate = Isolate::GetCurrent();
  HandleScope scope(isolate);

  // ... code goes here ...
  args.GetReturnValue().Set(foo);
}

Exposing Native Functions to JavaScript

// Rust

#[v8_fn]
fn my_function(args: FunctionCallbackInfo) {
  // ... code goes here ...
  args.set_return_value(foo);
}

Setting a property on an Object

// C++

Local<String> name = String::NewFromUtf8(
    isolate,
    "value",
    v8::NewStringType::kNormal
  ).ToLocalChecked();
Local<Number> num = Number::New(isolate, 42);
obj->Set(context, name, num);

Setting a property on an Object

// Rust

obj.set("value", 42);

Worker

( T:CT ⇒ tokio::runtime::current_thread )

Main Thread(s)

Main Thread(s)

The Future of Osgood

Database Support (e.g. PostgreSQL)

  • Configure connections in App file
  • Keep connection details out of application code
  • Whitelist approach depends on implementation
  • Expose a Query Builder to application?
    • policy.sql.allowRead('users')
  • Or implement via IndexedDB?
    • policy.sql.allow('SELECT * FROM users')

Transport Features

  • gzip / TLS for incoming requests
    • Outgoing gzip and TLS works today
  • HTTP2
  • Incoming WebSockets, or an RPC like gRPC?

Disk I/O

  • import template from "./template.hbs";
  • Filesystem abstractions (e.g. CacheStorage API)
  • Or ‘chroot’ like this hand-wavey example:
// app.js
app.get('/', 'worker.js', policy => {
  policy.chroot('~/.osgood/', 'config', 'readonly');
});

// worker.js
import file from 'osgood:filesystem';
const conf = await file.read('config', '/conf.json');

Module Support

  • Certain npm modules can be run through webpack
    • Output a file that uses export
  • Any Node.js-only features aren't compatible
    • Just think require('child_process');
  • TODO: Come up with a hands-free solution
  • We built an XHR polyfill and got AWS SDK working

Fin