Support TCP port exposure via "--expose" command-line argument (#70)
This commit is contained in:
parent
f3acef87a7
commit
8519fa2f86
|
|
@ -303,6 +303,19 @@ dependencies = [
|
|||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "5.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"hashbrown 0.14.5",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.6.0"
|
||||
|
|
@ -565,6 +578,12 @@ dependencies = [
|
|||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.2"
|
||||
|
|
@ -900,7 +919,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -960,6 +979,16 @@ version = "0.7.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.22"
|
||||
|
|
@ -1218,6 +1247,30 @@ dependencies = [
|
|||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oslog"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80d2043d1f61d77cb2f4b1f7b7b2295f40507f5f8e9d1c8bf10a1ca5f97a3969"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"dashmap",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
|
|
@ -1389,6 +1442,15 @@ dependencies = [
|
|||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.0"
|
||||
|
|
@ -1516,6 +1578,12 @@ dependencies = [
|
|||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
|
|
@ -1765,9 +1833,11 @@ dependencies = [
|
|||
"ip_network",
|
||||
"ipnet",
|
||||
"libc",
|
||||
"log",
|
||||
"mac_address",
|
||||
"nix 0.29.0",
|
||||
"num_enum 0.7.3",
|
||||
"oslog",
|
||||
"polling",
|
||||
"prefix-trie",
|
||||
"privdrop",
|
||||
|
|
@ -2153,9 +2223,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||
|
||||
[[package]]
|
||||
name = "vmnet"
|
||||
version = "0.3.0"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2063a13f916de87aa0946f4af0fa68956a0712f8f2e7994b49a70b06ecc06377"
|
||||
checksum = "c96eae216a9fa27c8e458ee5ef56dfc92d43fc8c25d67ade4a7fa83f0cb8b21b"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"block",
|
||||
|
|
@ -2171,9 +2241,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "vmnet-derive"
|
||||
version = "0.3.0"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96a04ced3e757af6aa37c079c3ce756a579d627904f37482342874c642e47068"
|
||||
checksum = "fa20b8d2b5b0504355848b99b54fda9411f8201f44c1a9ee5a2d3c3134d31297"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
|
|
|
|||
|
|
@ -30,3 +30,5 @@ sentry-anyhow = { version = "0", features = ["backtrace"] }
|
|||
nix = { version = "0", features = ["signal"] }
|
||||
prefix-trie = "0"
|
||||
ipnet = "2"
|
||||
oslog = "0.2.0"
|
||||
log = "0.4.22"
|
||||
|
|
|
|||
|
|
@ -73,7 +73,15 @@ impl Lease {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn address(&self) -> Ipv4Address {
|
||||
self.address
|
||||
}
|
||||
|
||||
pub fn valid(&self) -> bool {
|
||||
Instant::now() < self.valid_until
|
||||
}
|
||||
|
||||
pub fn valid_ip_source(&self, address: Ipv4Address) -> bool {
|
||||
self.address == address && Instant::now() < self.valid_until
|
||||
self.address == address && self.valid()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
31
lib/host.rs
31
lib/host.rs
|
|
@ -1,5 +1,6 @@
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
use clap::ValueEnum;
|
||||
use log::info;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
use std::os::unix::net::UnixDatagram;
|
||||
|
|
@ -7,6 +8,7 @@ use std::str::FromStr;
|
|||
use std::sync::mpsc::{sync_channel, SyncSender};
|
||||
use vmnet::mode::Mode;
|
||||
use vmnet::parameters::{Parameter, ParameterKind};
|
||||
use vmnet::port_forwarding::{AddressFamily, Protocol};
|
||||
use vmnet::{Events, Options};
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug)]
|
||||
|
|
@ -100,6 +102,35 @@ impl Host {
|
|||
}
|
||||
|
||||
impl Host {
|
||||
pub fn port_forwarding_add_rule(
|
||||
&mut self,
|
||||
external_port: u16,
|
||||
internal_addr: Ipv4Addr,
|
||||
internal_port: u16,
|
||||
) -> Result<()> {
|
||||
let details = format!("external_port={external_port}, internal_addr={internal_addr}, internal_port={internal_port}");
|
||||
|
||||
self.interface
|
||||
.port_forwarding_rule_add(
|
||||
AddressFamily::Ipv4,
|
||||
Protocol::Tcp,
|
||||
external_port,
|
||||
internal_addr.into(),
|
||||
internal_port,
|
||||
)
|
||||
.map(|_| info!("added port forwarding rule {details}"))
|
||||
.map_err(|err| anyhow!("failed to add port forwarding rule {details}: {err}"))
|
||||
}
|
||||
|
||||
pub fn port_forwarding_remove_rule(&mut self, external_port: u16) -> Result<()> {
|
||||
let details = format!("external_port={external_port}");
|
||||
|
||||
self.interface
|
||||
.port_forwarding_rule_remove(AddressFamily::Ipv4, Protocol::Tcp, external_port)
|
||||
.map(|_| info!("removed port forwarding rule {details}"))
|
||||
.map_err(|err| anyhow!("failed to remove port forwarding rule {details}: {err}"))
|
||||
}
|
||||
|
||||
pub fn read(&mut self, buf: &mut [u8]) -> vmnet::Result<usize> {
|
||||
// Dequeue dummy datagram from the socket (if any)
|
||||
// to free up buffer space and reduce false-positives
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
use anyhow::{anyhow, Context, Error};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct ExposedPort {
|
||||
pub external_port: u16,
|
||||
pub internal_port: u16,
|
||||
}
|
||||
|
||||
impl FromStr for ExposedPort {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let splits: Vec<&str> = s.split(':').collect();
|
||||
|
||||
match splits.len() {
|
||||
2 => Ok(ExposedPort {
|
||||
external_port: splits[0]
|
||||
.parse()
|
||||
.context(format!("invalid external port {:?}", splits[0]))?,
|
||||
internal_port: splits[1]
|
||||
.parse()
|
||||
.context(format!("invalid internal port {:?}", splits[1]))?,
|
||||
}),
|
||||
_ => Err(anyhow!(
|
||||
"invalid exposed port specification {:?}, the format should be EXTERNAL:INTERNAL",
|
||||
s
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::proxy::exposed_port::ExposedPort;
|
||||
|
||||
#[test]
|
||||
fn exposed_port() {
|
||||
assert_eq!(
|
||||
ExposedPort {
|
||||
external_port: 2222,
|
||||
internal_port: 22
|
||||
},
|
||||
"2222:22".parse().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
mod exposed_port;
|
||||
mod host;
|
||||
mod port_forwarder;
|
||||
mod udp_packet_helper;
|
||||
mod vm;
|
||||
|
||||
|
|
@ -8,8 +10,10 @@ use crate::host::NetType;
|
|||
use crate::poller::Poller;
|
||||
use crate::vm::VM;
|
||||
use anyhow::Result;
|
||||
pub use exposed_port::ExposedPort;
|
||||
use ipnet::Ipv4Net;
|
||||
use mac_address::MacAddress;
|
||||
use port_forwarder::PortForwarder;
|
||||
use prefix_trie::{Prefix, PrefixSet};
|
||||
use smoltcp::wire::EthernetFrame;
|
||||
use std::io::ErrorKind;
|
||||
|
|
@ -23,6 +27,7 @@ pub struct Proxy<'proxy> {
|
|||
dhcp_snooper: DhcpSnooper,
|
||||
allow: PrefixSet<Ipv4Net>,
|
||||
enobufs_encountered: bool,
|
||||
port_forwarder: PortForwarder,
|
||||
}
|
||||
|
||||
impl Proxy<'_> {
|
||||
|
|
@ -31,6 +36,7 @@ impl Proxy<'_> {
|
|||
vm_mac_address: MacAddress,
|
||||
vm_net_type: NetType,
|
||||
allow: PrefixSet<Ipv4Net>,
|
||||
exposed_ports: Vec<ExposedPort>,
|
||||
) -> Result<Proxy<'proxy>> {
|
||||
let vm = VM::new(vm_fd)?;
|
||||
let host = Host::new(vm_net_type, !allow.contains(&Ipv4Net::zero()))?;
|
||||
|
|
@ -44,6 +50,7 @@ impl Proxy<'_> {
|
|||
dhcp_snooper: Default::default(),
|
||||
allow,
|
||||
enobufs_encountered: false,
|
||||
port_forwarder: PortForwarder::new(exposed_ports),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -68,6 +75,12 @@ impl Proxy<'_> {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
// Timeout
|
||||
if !vm_readable && !host_readable && !interrupt {
|
||||
self.port_forwarder
|
||||
.tick(&mut self.host, self.dhcp_snooper.lease());
|
||||
}
|
||||
|
||||
self.poller.rearm()?;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
use crate::dhcp_snooper::Lease;
|
||||
use crate::host::Host;
|
||||
use crate::proxy::exposed_port::ExposedPort;
|
||||
use anyhow::Result;
|
||||
use log::error;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PortForwarder {
|
||||
port_forwardings: Vec<PortForwarding>,
|
||||
failed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
struct PortForwarding {
|
||||
exposed_port: ExposedPort,
|
||||
forwarding_to_addr: Option<Ipv4Addr>,
|
||||
}
|
||||
|
||||
impl PortForwarder {
|
||||
pub fn new(exposed_ports: Vec<ExposedPort>) -> PortForwarder {
|
||||
let port_forwardings = exposed_ports
|
||||
.into_iter()
|
||||
.map(|exposed_port| PortForwarding {
|
||||
exposed_port,
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
|
||||
PortForwarder {
|
||||
port_forwardings,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, host: &mut Host, lease: &Option<Lease>) {
|
||||
if self.failed {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = self.tick_inner(host, lease) {
|
||||
error!("port-forwarding failed: {}", err);
|
||||
|
||||
self.failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn tick_inner(&mut self, host: &mut Host, lease: &Option<Lease>) -> Result<()> {
|
||||
if let Some(lease) = lease {
|
||||
// Lease exists, but is not valid, remove all port forwardings
|
||||
if !lease.valid() {
|
||||
self.remove_all_port_forwardings(host)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Lease exists and is valid, install/re-install port forwardings
|
||||
for port_forwarding in &mut self.port_forwardings {
|
||||
if let Some(installed_addr) = port_forwarding.forwarding_to_addr {
|
||||
// Port forwarding already installed, perhaps it's outdated?
|
||||
if installed_addr == lease.address() {
|
||||
// Nope, the port forwarding is up to date
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove port forwarding since the lease address had changed
|
||||
host.port_forwarding_remove_rule(port_forwarding.exposed_port.external_port)?;
|
||||
port_forwarding.forwarding_to_addr = None;
|
||||
}
|
||||
|
||||
// Install new port forwarding
|
||||
host.port_forwarding_add_rule(
|
||||
port_forwarding.exposed_port.external_port,
|
||||
lease.address(),
|
||||
port_forwarding.exposed_port.internal_port,
|
||||
)?;
|
||||
port_forwarding.forwarding_to_addr = Some(lease.address());
|
||||
}
|
||||
} else {
|
||||
// Lease does not exist, remove all port forwardings
|
||||
self.remove_all_port_forwardings(host)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_all_port_forwardings(&mut self, host: &mut Host) -> Result<()> {
|
||||
for port_forwarding in &mut self.port_forwardings {
|
||||
if port_forwarding.forwarding_to_addr.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
host.port_forwarding_remove_rule(port_forwarding.exposed_port.external_port)?;
|
||||
port_forwarding.forwarding_to_addr = None;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
19
src/main.rs
19
src/main.rs
|
|
@ -1,14 +1,16 @@
|
|||
use anyhow::{anyhow, Context};
|
||||
use clap::Parser;
|
||||
use ipnet::Ipv4Net;
|
||||
use log::LevelFilter;
|
||||
use nix::sys::signal::{signal, SigHandler, Signal};
|
||||
use oslog::OsLogger;
|
||||
use prefix_trie::PrefixSet;
|
||||
use privdrop::PrivDrop;
|
||||
use softnet::proxy::ExposedPort;
|
||||
use softnet::proxy::Proxy;
|
||||
use softnet::NetType;
|
||||
use std::borrow::Cow;
|
||||
use std::env;
|
||||
|
||||
use std::os::raw::c_int;
|
||||
use std::os::unix::io::RawFd;
|
||||
use std::os::unix::process::CommandExt;
|
||||
|
|
@ -57,6 +59,15 @@ struct Args {
|
|||
)]
|
||||
allow: Vec<Ipv4Net>,
|
||||
|
||||
#[clap(
|
||||
long,
|
||||
help = "comma-separated list of TCP ports to expose (e.g. --expose 2222:22,8080:80)",
|
||||
value_name = "comma-separated port specifications",
|
||||
use_value_delimiter = true,
|
||||
action = clap::ArgAction::Set
|
||||
)]
|
||||
expose: Vec<ExposedPort>,
|
||||
|
||||
#[clap(long, hide = true)]
|
||||
sudo_escalation_probing: bool,
|
||||
|
||||
|
|
@ -101,6 +112,11 @@ fn main() -> ExitCode {
|
|||
}
|
||||
|
||||
fn try_main() -> anyhow::Result<()> {
|
||||
// Initialize logger
|
||||
OsLogger::new("org.cirruslabs.softnet")
|
||||
.level_filter(LevelFilter::Info)
|
||||
.init()?;
|
||||
|
||||
// The default signal(3)[1] action for SIGINT is to interrupt program,
|
||||
// but we want to handle SIGINT ourselves, so we ignore it. The kqueue(2)'s[2]
|
||||
// EVFILT_SIGNAL will receive it anyways, because it has lower precedence.
|
||||
|
|
@ -161,6 +177,7 @@ fn try_main() -> anyhow::Result<()> {
|
|||
args.vm_mac_address,
|
||||
args.vm_net_type,
|
||||
PrefixSet::from_iter(args.allow),
|
||||
args.expose,
|
||||
)
|
||||
.context("failed to initialize proxy")?;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue