Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ oslog = "0.2.0"
log = "0.4.29"
serial_test = "3"
coarsetime = "0.1.37"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
jsonrpsee-types = "0.26"

[profile.release]
debug = true
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,18 @@ For proper functioning, Softnet binary requires two things:
## Running

Softnet is started and managed automatically by Tart if `--net-softnet` flag is provided when calling `tart run`.

### Dynamic network policy

Softnet can update the running VM's IPv4 egress policy without restarting the VM. Pass a connected Unix stream socket as `--control-fd` to enable a newline-delimited [JSON-RPC 2.0](https://www.jsonrpc.org/specification) control channel. The socket is duplex and must be separate from `--vm-fd`, which carries VM packets.

The supported methods are `softnet.policy.get` and `softnet.policy.set`. A complete policy update looks like this (each request and response occupies one line):

```json
{"jsonrpc":"2.0","id":"42","method":"softnet.policy.set","params":{"allow":["@host","10.0.0.0/8"],"block":["0.0.0.0/0"]}}
{"jsonrpc":"2.0","id":"42","result":{"allow":["10.0.0.0/8","@host"],"block":["0.0.0.0/0"],"ruleCount":3}}
```

Every request must include a non-null string (at most 256 bytes) or non-negative integer `id`; notifications are rejected so policy changes always have an acknowledgment. Policy updates are atomic: all targets are parsed and a new prefix map is built before the active policy changes. Longest-prefix matching and block precedence for identical prefixes are preserved. Targets are normalized and deduplicated. A policy update may contain at most 4096 combined allow/block targets, and a request frame may not exceed 1 MiB.

Use `block=["0.0.0.0/0"]` with specific allow targets for a default-deny policy. Closing the control socket leaves the last accepted policy active.
21 changes: 20 additions & 1 deletion lib/poller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,23 @@ pub struct Poller<'poller> {
timeout: Duration,
vm_fd: BorrowedFd<'poller>,
host_fd: BorrowedFd<'poller>,
control_fd: Option<BorrowedFd<'poller>>,
}

#[derive(IntoPrimitive)]
#[repr(usize)]
enum EventKey {
VM,
Host,
Control,
Interrupt,
}

impl Poller<'_> {
pub fn new<'poller>(
vm_fd: RawFd,
host_fd: RawFd,
control_fd: Option<RawFd>,
timeout: Duration,
) -> Result<Poller<'poller>> {
let poller = polling::Poller::new()?;
Expand All @@ -36,6 +39,7 @@ impl Poller<'_> {
timeout,
vm_fd: unsafe { BorrowedFd::borrow_raw(vm_fd) },
host_fd: unsafe { BorrowedFd::borrow_raw(host_fd) },
control_fd: control_fd.map(|fd| unsafe { BorrowedFd::borrow_raw(fd) }),
})
}

Expand All @@ -46,6 +50,14 @@ impl Poller<'_> {
self.vm_interest(),
PollMode::Edge,
)?;

if let Some(control_fd) = self.control_fd {
self.poller.add_with_mode(
control_fd.as_raw_fd(),
polling::Event::all(EventKey::Control.into()),
PollMode::Edge,
)?;
}
self.poller.add_with_mode(
self.host_fd.as_raw_fd(),
self.host_interest(),
Expand Down Expand Up @@ -79,10 +91,17 @@ impl Poller<'_> {
.events
.iter()
.any(|ev| ev.key == Into::<usize>::into(EventKey::Interrupt));

Ok((vm_readable, host_readable, interrupt))
}

pub fn remove_control(&mut self) -> Result<()> {
if let Some(control_fd) = self.control_fd.take() {
self.poller.delete(control_fd)?;
}

Ok(())
}

fn vm_interest(&self) -> polling::Event {
polling::Event::readable(EventKey::VM.into())
}
Expand Down
Loading