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
116 changes: 114 additions & 2 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 @@ -13,17 +13,20 @@ debug = true

[dependencies]
smoltcp = "0"
etherparse = "0.21"
libc = "0"
tokio = { version = "1", features = ["rt-multi-thread", "sync", "net", "signal", "time", "macros", "io-util"] }
tokio-util = { version = "0.7.19", features = ["join-map", "codec"] }
bytes = "1"
futures-util = { version = "0.3", default-features = false, features = ["std"] }
dhcproto = { git = "https://github.com/bluecatengineering/dhcproto.git", branch = "master" }
hickory-server = { version = "0.26.1", default-features = false }
vmnet = "0.10.0"
clap = { version = "4", features = ["derive"] }
mac_address = "1"
privdrop = "0"
anyhow = { version = "1", features = ["backtrace"] }
async-trait = "0.1"
thiserror = "2"
ip_network = "0"
uzers = "0"
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,16 @@ Softnet can start with just `--control-fd` and create VMs through RPC.

`softnet.initialize` accepts `vmNetType` (`"nat"` or `"host"`, default `"nat"`) and a `vms` array containing one to five VMs. Each VM requires a distinct `vmMacAddress` and accepts optional `allow`, `block`, and `expose` arrays, which default to empty. Rules and exposed TCP ports use the same syntax as the corresponding CLI arguments.

Each VM may specify a unique single-label DNS `name`. Softnet answers queries for `<name>.internal` with that VM's reserved IPv4 address. It serves the shared authoritative `internal.` zone over UDP only, bypassing allow/block rules for these DNS queries. Other DNS queries use the system's DNS server.

Attach exactly one VM socket descriptor per entry through `SCM_RIGHTS` to the first byte of the request frame, in the same order as `vms`. Keep the sender's descriptors open until the initialization response arrives.

A successful response contains a `vms` array in request order; each entry includes a UUID string `vmId` and the corresponding `vmMacAddress`. Initialization drops privileges before responding.

With two attached VM socket descriptors, a request and response look like this (each occupies one line; VM IDs are illustrative):

```json
{"jsonrpc":"2.0","id":"42","method":"softnet.initialize","params":{"vmNetType":"nat","vms":[{"vmMacAddress":"02:00:00:00:00:02","allow":["@host"]},{"vmMacAddress":"02:00:00:00:00:03"}]}}
{"jsonrpc":"2.0","id":"42","method":"softnet.initialize","params":{"vmNetType":"nat","vms":[{"vmMacAddress":"02:00:00:00:00:02","name":"vm1","allow":["@host"]},{"vmMacAddress":"02:00:00:00:00:03","name":"vm2"}]}}
{"jsonrpc":"2.0","id":"42","result":{"vms":[{"vmId":"019a1b2c-3d4e-7000-8000-000000000001","vmMacAddress":"02:00:00:00:00:02"},{"vmId":"019a1b2c-3d4e-7000-8000-000000000002","vmMacAddress":"02:00:00:00:00:03"}]}}
```

Expand Down
17 changes: 16 additions & 1 deletion lib/control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::manager::{Call, CallError, Reply, Request, VmConfig};
use crate::network::MAX_VMS;
use crate::proxy::Rule;
use anyhow::Result;
use hickory_server::proto::rr::Name;
use jsonrpsee_types::{
ErrorObjectOwned, Id, Request as RpcRequest, Response, ResponsePayload,
error::{
Expand Down Expand Up @@ -40,6 +41,9 @@ struct VmParams {
// Unique MAC address used by this VM
vm_mac_address: String,

// Single-label VM name registered in our DNS zone
name: Option<String>,

// Initial packet filter allow rules
#[serde(default)]
allow: Vec<String>,
Expand Down Expand Up @@ -130,7 +134,7 @@ impl Control {
"block": policy.block.iter().map(ToString::to_string).collect::<Vec<_>>(),
"ruleCount": policy.rule_count,
})),
Err(CallError::InvalidVm(error)) => {
Err(CallError::InvalidParams(error)) => {
Err(rpc_error(INVALID_PARAMS_CODE, error.to_string()))
}
Err(CallError::Failed(error)) => {
Expand Down Expand Up @@ -306,6 +310,16 @@ fn parse_initialize(
.vm_mac_address
.parse()
.map_err(|_| rpc_error(INVALID_PARAMS_CODE, "invalid vmMacAddress"))?;

// Parse optional VM names for DNS lookup
let name = params
.name
.map(|name| {
Name::from_ascii(name)
.map_err(|_| rpc_error(INVALID_PARAMS_CODE, "invalid name"))
})
.transpose()?;

let (allow, block) = parse_policy(params.allow, params.block)?;
let exposed_ports = params
.expose
Expand All @@ -320,6 +334,7 @@ fn parse_initialize(
Ok(VmConfig {
vm_fd: fd,
vm_mac_address,
name,
allow,
block,
exposed_ports,
Expand Down
14 changes: 11 additions & 3 deletions lib/dhcp_server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use anyhow::Result;
use dhcproto::v4::{
CLIENT_PORT, DhcpOption, Flags, HType, Message, MessageType, Opcode, OptionCode, SERVER_PORT,
};
use dhcproto::{Decodable, Encodable};
use dhcproto::{Decodable, Encodable, Name};
use smoltcp::phy::ChecksumCapabilities;
use smoltcp::wire::{
ETHERNET_HEADER_LEN, EthernetAddress, EthernetFrame, EthernetProtocol, IpAddress, IpProtocol,
Expand All @@ -16,6 +16,7 @@ pub(crate) struct DhcpServer {
vm_ip: Ipv4Address,
subnet_mask: Ipv4Address,
gateway_ip: Ipv4Address,
search_domains: Vec<Name>,
}

impl DhcpServer {
Expand All @@ -25,12 +26,14 @@ impl DhcpServer {
vm_ip: Ipv4Address,
subnet_mask: Ipv4Address,
gateway_ip: Ipv4Address,
search_domains: Vec<Name>,
) -> Self {
Self {
vm_mac,
vm_ip,
subnet_mask,
gateway_ip,
search_domains,
}
}

Expand Down Expand Up @@ -166,6 +169,11 @@ impl DhcpServer {
options.insert(DhcpOption::DomainNameServer(vec![self.gateway_ip]));
options.insert(DhcpOption::AddressLeaseTime(u32::MAX));

// Advertise the configured DNS search domains for short-name resolution
if !self.search_domains.is_empty() {
options.insert(DhcpOption::DomainSearch(self.search_domains.clone()));
}

reply
}

Expand Down Expand Up @@ -275,7 +283,7 @@ mod tests {
#[test]
fn assigns_reserved_address() {
// Create a server and a client request
let dhcp = DhcpServer::new(VM, ADDRESS, MASK, GATEWAY);
let dhcp = DhcpServer::new(VM, ADDRESS, MASK, GATEWAY, Vec::new());
let mut message = request(MessageType::Discover);

// Discover the reserved address
Expand Down Expand Up @@ -318,7 +326,7 @@ mod tests {
#[test]
fn renews_reserved_address_and_rejects_stale_address() {
// Renew the reserved address
let dhcp = DhcpServer::new(VM, ADDRESS, MASK, GATEWAY);
let dhcp = DhcpServer::new(VM, ADDRESS, MASK, GATEWAY, Vec::new());
let mut message = request(MessageType::Request);
message.set_ciaddr(ADDRESS);
let (ack, destination) = exchange(&dhcp, &message, GATEWAY);
Expand Down
Loading
Loading