Skip to content

Commit

Permalink
Add support for TLS sockets
Browse files Browse the repository at this point in the history
This adds the module std.net.tls and refactors std.net.socket in various
places, such that we can provide support for TLS 1.2 and TLS 1.3.

The TLS stack is backed by Rustls (https://github.com/rustls/rustls). My
original plan was to write the stack in Inko, but I deemed this far too
time consuming and not beneficial for users (compared to using an
existing mature stack). I also experimented with OpenSSL, but using
OpenSSL is like walking through a minefield, and its API is a pain to
use (in part due to its use of global and thread-local state).

Rustls is compiled such that it uses the "ring" backend instead of
aws-lc. This is done because aws-lc requires additional dependencies on
FreeBSD, and increases compile times significantly (about 30 seconds or
so). While performance of TLS 1.3 is less ideal when using ring compared
to using aws-lc (rustls/rustls#1751), it
should still be good enough (and still be much faster compared to using
OpenSSL).

A downside of using Rustls is that the executable sizes increase by
about 6 MiB (or 2 MiB when stripping them), due to the extra code
introduced by Rustls and its dependencies. Sadly we can't avoid this
unless we use OpenSSL, which introduces far more pressing issues.

This fixes #329.

Changelog: added
  • Loading branch information
yorickpeterse committed Jul 19, 2024
1 parent 915b00c commit 6cbe93c
Show file tree
Hide file tree
Showing 34 changed files with 2,803 additions and 770 deletions.
422 changes: 328 additions & 94 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion compiler/src/linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,13 @@ pub(crate) fn link(
cmd.arg("-lm");
cmd.arg("-lpthread");
}
_ => {}
OperatingSystem::Mac => {
// This is needed for TLS support.
for name in ["Security", "CoreFoundation"] {
cmd.arg("-framework");
cmd.arg(name);
}
}
}

let mut static_linking = state.config.static_linking;
Expand Down
13 changes: 13 additions & 0 deletions rt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ unicode-segmentation = "^1.10"
backtrace = "^0.3"
rustix = { version = "^0.38", features = ["fs", "mm", "param", "process", "net", "std", "time", "event"], default-features = false }

# The dependencies needed for TLS support.
#
# We use ring instead of the default aws-lc-sys because:
#
# 1. aws-lc-sys requires cmake to be installed when building on FreeBSD (and
# potentially other platforms), as aws-lc-sys only provides generated
# bindings for a limited set of platforms
# 2. aws-lc-sys increases compile times quite a bit
# 3. We don't care about FIPS compliance at the time of writing
rustls = { version = "^0.23", features = ["ring", "tls12", "std"], default-features = false }
rustls-platform-verifier = "^0.3"
rustls-pemfile = "^2.1"

[dependencies.socket2]
version = "^0.5"
features = ["all"]
1 change: 1 addition & 0 deletions rt/src/network_poller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const CAPACITY: usize = 1024;
pub(crate) type NetworkPoller = sys::Poller;

/// The type of event a poller should wait for.
#[derive(Debug)]
pub(crate) enum Interest {
Read,
Write,
Expand Down
20 changes: 11 additions & 9 deletions rt/src/network_poller/kqueue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,20 @@ impl Poller {
source: impl AsFd,
interest: Interest,
) {
let fd = source.as_fd().as_raw_fd();
let (add, del) = match interest {
Interest::Read => (EventFilter::Read(fd), EventFilter::Write(fd)),
Interest::Write => (EventFilter::Write(fd), EventFilter::Read(fd)),
};
let id = process.identifier() as isize;
let fd = source.as_fd().as_raw_fd();
let flags =
EventFlags::CLEAR | EventFlags::ONESHOT | EventFlags::RECEIPT;
let events = [
Event::new(add, EventFlags::ADD | flags, id),
Event::new(del, EventFlags::DELETE, 0),
];
let events = match interest {
Interest::Read => [
Event::new(EventFilter::Read(fd), EventFlags::ADD | flags, id),
Event::new(EventFilter::Write(fd), EventFlags::DELETE, 0),
],
Interest::Write => [
Event::new(EventFilter::Write(fd), EventFlags::ADD | flags, id),
Event::new(EventFilter::Read(fd), EventFlags::DELETE, 0),
],
};

self.apply(&events);
}
Expand Down
8 changes: 6 additions & 2 deletions rt/src/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ pub(crate) fn error_to_int(error: io::Error) -> i64 {
// raw_os_error() above returns a None.
Errno::TIMEDOUT.raw_os_error()
} else {
-1
match error.kind() {
io::ErrorKind::InvalidData => -2,
io::ErrorKind::UnexpectedEof => -3,
_ => -1,
}
};

code as i64
Expand Down Expand Up @@ -60,7 +64,7 @@ impl Result {
}

pub(crate) fn io_error(error: io::Error) -> Result {
Self::error({ error_to_int(error) } as _)
Self::error(error_to_int(error) as _)
}
}

Expand Down
7 changes: 7 additions & 0 deletions rt/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod stdio;
mod string;
mod sys;
mod time;
mod tls;

use crate::config::Config;
use crate::mem::ClassPointer;
Expand Down Expand Up @@ -67,6 +68,12 @@ pub unsafe extern "system" fn inko_runtime_new(
// does for us when compiling an executable.
signal_sched::block_all();

// Configure the TLS provider. This must be done once before we start the
// program.
rustls::crypto::ring::default_provider()
.install_default()
.expect("failed to set up the default TLS cryptography provider");

Box::into_raw(Box::new(Runtime::new(&*counts, args)))
}

Expand Down
20 changes: 0 additions & 20 deletions rt/src/runtime/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,26 +40,6 @@ pub unsafe extern "system" fn inko_env_size(state: *const State) -> i64 {
(*state).environment.len() as _
}

#[no_mangle]
pub unsafe extern "system" fn inko_env_home_directory(
state: *const State,
) -> InkoResult {
let state = &*state;

// Rather than performing all sorts of magical incantations to get the home
// directory, we're just going to require that HOME is set.
//
// If the home is explicitly set to an empty string we still ignore it,
// because there's no scenario in which Some("") is useful.
state
.environment
.get("HOME")
.filter(|&path| !path.is_empty())
.cloned()
.map(|v| InkoResult::ok(InkoString::alloc(state.string_class, v) as _))
.unwrap_or_else(InkoResult::none)
}

#[no_mangle]
pub unsafe extern "system" fn inko_env_temp_directory(
state: *const State,
Expand Down
51 changes: 51 additions & 0 deletions rt/src/runtime/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
use crate::context;
use crate::network_poller::Interest;
use crate::process::ProcessPointer;
use crate::scheduler::timeouts::Timeout;
use crate::socket::Socket;
use crate::state::State;
use std::io::{self, Read};

/// Reads a number of bytes from a buffer into a Vec.
Expand All @@ -14,3 +20,48 @@ pub(crate) fn read_into<T: Read>(

Ok(read as i64)
}

pub(crate) fn poll(
state: &State,
mut process: ProcessPointer,
socket: &mut Socket,
interest: Interest,
deadline: i64,
) -> io::Result<()> {
let poll_id = unsafe { process.thread() }.network_poller;

// We must keep the process' state lock open until everything is registered,
// otherwise a timeout thread may reschedule the process (i.e. the timeout
// is very short) before we finish registering the socket with a poller.
{
let mut proc_state = process.state();

// A deadline of -1 signals that we should wait indefinitely.
if deadline >= 0 {
let time = Timeout::until(deadline as u64);

proc_state.waiting_for_io(Some(time.clone()));
state.timeout_worker.suspend(process, time);
} else {
proc_state.waiting_for_io(None);
}

socket.register(state, process, poll_id, interest);
}

// Safety: the current thread is holding on to the process' run lock, so if
// the process gets rescheduled onto a different thread, said thread won't
// be able to use it until we finish this context switch.
unsafe { context::switch(process) };

if process.timeout_expired() {
// The socket is still registered at this point, so we have to
// deregister first. If we don't and suspend for another IO operation,
// the poller could end up rescheduling the process multiple times (as
// there are multiple events still in flight for the process).
socket.deregister(state);
return Err(io::Error::from(io::ErrorKind::TimedOut));
}

Ok(())
}
96 changes: 27 additions & 69 deletions rt/src/runtime/socket.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
use crate::context;
use crate::mem::{ByteArray, String as InkoString};
use crate::network_poller::Interest;
use crate::process::ProcessPointer;
use crate::result::{error_to_int, Result};
use crate::scheduler::timeouts::Timeout;
use crate::socket::Socket;
use crate::runtime::helpers::poll;
use crate::socket::{read_from, Socket};
use crate::state::State;
use std::io::{self, Write};
use std::ptr::{drop_in_place, write};
Expand All @@ -24,66 +23,33 @@ impl RawAddress {
}
}

fn blocking<T>(
fn run<T>(
state: &State,
mut process: ProcessPointer,
process: ProcessPointer,
socket: &mut Socket,
interest: Interest,
deadline: i64,
mut func: impl FnMut(&mut Socket) -> io::Result<T>,
) -> io::Result<T> {
match func(socket) {
Err(err) if err.kind() == io::ErrorKind::WouldBlock => {}
val => return val,
}

let poll_id = unsafe { process.thread() }.network_poller;

// We must keep the process' state lock open until everything is registered,
// otherwise a timeout thread may reschedule the process (i.e. the timeout
// is very short) before we finish registering the socket with a poller.
{
let mut proc_state = process.state();

// A deadline of -1 signals that we should wait indefinitely.
if deadline >= 0 {
let time = Timeout::until(deadline as u64);

proc_state.waiting_for_io(Some(time.clone()));
state.timeout_worker.suspend(process, time);
} else {
proc_state.waiting_for_io(None);
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
poll(state, process, socket, interest, deadline)
.and_then(|_| func(socket))
}

socket.register(state, process, poll_id, interest);
val => val,
}

// Safety: the current thread is holding on to the process' run lock, so if
// the process gets rescheduled onto a different thread, said thread won't
// be able to use it until we finish this context switch.
unsafe { context::switch(process) };

if process.timeout_expired() {
// The socket is still registered at this point, so we have to
// deregister first. If we don't and suspend for another IO operation,
// the poller could end up rescheduling the process multiple times (as
// there are multiple events still in flight for the process).
socket.deregister(state);
return Err(io::Error::from(io::ErrorKind::TimedOut));
}

func(socket)
}

#[no_mangle]
pub(crate) unsafe extern "system" fn inko_socket_new(
proto: i64,
domain: i64,
kind: i64,
proto: i64,
out: *mut Socket,
) -> i64 {
let sock = match proto {
0 => Socket::ipv4(kind),
1 => Socket::ipv6(kind),
let sock = match domain {
0 => Socket::ipv4(kind, proto),
1 => Socket::ipv6(kind, proto),
_ => Socket::unix(kind),
};

Expand All @@ -108,7 +74,7 @@ pub(crate) unsafe extern "system" fn inko_socket_write(
let state = &*state;
let slice = std::slice::from_raw_parts(data, size as _);

blocking(state, process, &mut *socket, Interest::Write, deadline, |sock| {
run(state, process, &mut *socket, Interest::Write, deadline, |sock| {
sock.write(slice)
})
.map(|v| Result::ok(v as _))
Expand All @@ -126,8 +92,8 @@ pub unsafe extern "system" fn inko_socket_read(
) -> Result {
let state = &*state;

blocking(state, process, &mut *socket, Interest::Read, deadline, |sock| {
sock.read(&mut (*buffer).value, amount as usize)
run(state, process, &mut *socket, Interest::Read, deadline, |sock| {
read_from(sock, &mut (*buffer).value, amount as usize)
})
.map(|size| Result::ok(size as _))
.unwrap_or_else(Result::io_error)
Expand Down Expand Up @@ -169,7 +135,7 @@ pub unsafe extern "system" fn inko_socket_connect(
) -> Result {
let state = &*state;

blocking(state, process, &mut *socket, Interest::Write, deadline, |sock| {
run(state, process, &mut *socket, Interest::Write, deadline, |sock| {
sock.connect(InkoString::read(address), port as u16)
})
.map(|_| Result::none())
Expand All @@ -184,14 +150,10 @@ pub unsafe extern "system" fn inko_socket_accept(
deadline: i64,
out: *mut Socket,
) -> i64 {
let res = blocking(
&*state,
process,
&mut *socket,
Interest::Read,
deadline,
|sock| sock.accept(),
);
let res =
run(&*state, process, &mut *socket, Interest::Read, deadline, |sock| {
sock.accept()
});

match res {
Ok(val) => {
Expand All @@ -213,14 +175,10 @@ pub unsafe extern "system" fn inko_socket_receive_from(
out: *mut RawAddress,
) -> i64 {
let state = &*state;
let res = blocking(
state,
process,
&mut *socket,
Interest::Read,
deadline,
|sock| sock.recv_from(&mut (*buffer).value, amount as _),
);
let res =
run(state, process, &mut *socket, Interest::Read, deadline, |sock| {
sock.recv_from(&mut (*buffer).value, amount as _)
});

match res {
Ok((addr, port)) => {
Expand All @@ -244,7 +202,7 @@ pub unsafe extern "system" fn inko_socket_send_bytes_to(
let state = &*state;
let addr = InkoString::read(address);

blocking(state, process, &mut *socket, Interest::Write, deadline, |sock| {
run(state, process, &mut *socket, Interest::Write, deadline, |sock| {
sock.send_to(&(*buffer).value, addr, port as _)
})
.map(|size| Result::ok(size as _))
Expand All @@ -264,7 +222,7 @@ pub unsafe extern "system" fn inko_socket_send_string_to(
let state = &*state;
let addr = InkoString::read(address);

blocking(state, process, &mut *socket, Interest::Write, deadline, |sock| {
run(state, process, &mut *socket, Interest::Write, deadline, |sock| {
sock.send_to(InkoString::read(buffer).as_bytes(), addr, port as _)
})
.map(|size| Result::ok(size as _))
Expand Down
Loading

0 comments on commit 6cbe93c

Please sign in to comment.