mod pf_route;
mod sys;
use crate::tunnel::illumos::pf_route::RouteSocket;
use crate::tunnel::{
AFTR_V4_ELEMENT, B4_V4_PREFIX_LEN, DesiredState, EncapsulationLimit, Observed, TunnelBackend,
TunnelError, TunnelUpdate,
};
use std::io;
use std::mem::MaybeUninit;
use std::{
ffi::{CStr, CString, c_char, c_uint, c_void},
net::{IpAddr, Ipv4Addr, Ipv6Addr},
os::fd::{AsRawFd, FromRawFd, OwnedFd},
};
use sys::*;
pub(crate) use sys::{
RTM_ADD, RTM_CHANGE, RTM_CHGADDR, RTM_DELADDR, RTM_DELETE, RTM_FREEADDR, RTM_IFINFO,
RTM_NEWADDR, rt_msghdr,
};
const B4_V4_NETMASK: Ipv4Addr = Ipv4Addr::from_bits(u32::MAX << (32 - B4_V4_PREFIX_LEN));
pub struct IllumosBackend {
cname: CString,
}
impl IllumosBackend {
pub fn new(name: String) -> Result<Self, std::ffi::NulError> {
let cname = std::ffi::CString::new(name)?;
Ok(Self { cname })
}
fn create_tunnel(
&self,
handle: &DladmHandle,
desired: &DesiredState,
) -> Result<u32, TunnelError> {
let mut params = build_tunnel_params(&desired.local_v6, &desired.remote_v6);
let status = unsafe {
dladm_iptun_create(
handle.ptr,
self.cname.as_ptr(),
&mut params,
DLADM_OPT_ACTIVE,
)
};
if status != DLADM_STATUS_OK {
return Err(TunnelError::CreationFailed(format!(
"dladm_iptun_create failed with status {}",
status
)));
}
tracing::debug!(link_id = params.link_id, "tunnel created");
Ok(params.link_id)
}
fn create_if(&self, handle: &IpadmHandle) -> Result<(), TunnelError> {
let src = self.cname.as_bytes_with_nul();
if src.len() > LIFNAMSIZ {
return Err(TunnelError::CreationFailed(format!(
"interface name {} bytes, exceeds LIFNAMSIZ ({})",
src.len(),
LIFNAMSIZ
)));
}
let mut name_buf: [c_char; LIFNAMSIZ] = [0; LIFNAMSIZ];
unsafe {
std::ptr::copy_nonoverlapping(
src.as_ptr(),
name_buf.as_mut_ptr().cast::<u8>(),
src.len(),
)
};
let status = unsafe {
ipadm_create_if(handle.ptr, name_buf.as_mut_ptr(), AF_INET, IPADM_OPT_ACTIVE)
};
if status != IPADM_STATUS_OK {
return Err(TunnelError::CreationFailed(format!(
"ipadm_create_if failed with status {}",
status
)));
}
tracing::debug!("ip interface assigned to tunnel");
Ok(())
}
fn get_tunnel_params(&self, handle: &DladmHandle) -> Result<Option<IpTunParams>, TunnelError> {
let (link_id, status) = self.name_to_linkid(handle);
if status == DLADM_STATUS_NOTFOUND {
return Ok(None);
}
if status != DLADM_STATUS_OK {
return Err(TunnelError::StatusCheckFailed(format!(
"failed to get linkid, dladm_name2info status: {}",
status
)));
}
let mut params: IpTunParams = unsafe { MaybeUninit::zeroed().assume_init() };
params.link_id = link_id;
let status = unsafe { dladm_iptun_getparams(handle.ptr, &mut params, DLADM_OPT_ACTIVE) };
if status != DLADM_STATUS_OK {
return Err(TunnelError::StatusCheckFailed(format!(
"dladm_iptun_getparams failed with status: {}",
status
)));
};
Ok(Some(params))
}
fn get_encapsulation_limit(
&self,
handle: &DladmHandle,
link_id: u32,
) -> Result<Option<u8>, TunnelError> {
let mut value = [0_u8; DLADM_PROP_VAL_MAX];
let mut values = [value.as_mut_ptr().cast::<c_char>()];
let mut value_count: c_uint = 1;
let status = unsafe {
dladm_get_linkprop(
handle.ptr,
link_id,
DLADM_PROP_VAL_CURRENT,
c"encaplimit".as_ptr(),
values.as_mut_ptr(),
&mut value_count,
)
};
if status != DLADM_STATUS_OK {
return Err(TunnelError::StatusCheckFailed(format!(
"reading encaplimit, dladm_get_linkprop status: {status}"
)));
}
let value = CStr::from_bytes_until_nul(&value)
.map_err(|e| TunnelError::StatusCheckFailed(format!("invalid encaplimit value: {e}")))?
.to_str()
.map_err(|e| TunnelError::StatusCheckFailed(format!("encaplimit is not UTF-8: {e}")))?
.parse::<u8>()
.map_err(|e| {
TunnelError::StatusCheckFailed(format!("invalid encaplimit number: {e}"))
})?;
Ok((value != 0).then_some(value))
}
fn set_encapsulation_limit(
&self,
handle: &DladmHandle,
link_id: u32,
encapsulation_limit: EncapsulationLimit,
) -> Result<(), u32> {
let value = match encapsulation_limit {
EncapsulationLimit::Disabled => 0,
EncapsulationLimit::Value(value) => value.get(),
};
let mut value = value.to_string().into_bytes();
value.push(0);
let mut values = [value.as_mut_ptr().cast::<c_char>()];
let status = unsafe {
dladm_set_linkprop(
handle.ptr,
link_id,
c"encaplimit".as_ptr(),
values.as_mut_ptr(),
1,
DLADM_OPT_ACTIVE,
)
};
if status != DLADM_STATUS_OK {
return Err(status);
}
Ok(())
}
fn is_admin_up(&self) -> Result<bool, TunnelError> {
let socket = open_inet_dgram_socket().map_err(|e| {
TunnelError::StatusCheckFailed(format!("opening interface flags socket: {e}"))
})?;
let fd = socket.as_raw_fd();
unsafe { is_up(fd, &self.cname) }
.map_err(|e| TunnelError::StatusCheckFailed(format!("reading interface flags: {e}")))
}
fn get_mtu(&self) -> Result<u32, TunnelError> {
let socket = open_inet_dgram_socket().map_err(|e| {
TunnelError::StatusCheckFailed(format!("opening interface MTU socket: {e}"))
})?;
let fd = socket.as_raw_fd();
unsafe { sys::get_mtu(fd, &self.cname) }
.map_err(|e| TunnelError::StatusCheckFailed(format!("reading interface MTU: {e}")))
}
fn delete_tunnel(&self, handle: &DladmHandle) -> Result<(), TunnelError> {
let (link_id, status) = self.name_to_linkid(handle);
if status != DLADM_STATUS_OK {
return Err(TunnelError::DestroyFailed(format!(
"failed to get linkid, dladm_name2info status: {}",
status
)));
}
tracing::debug!(link_id, "resolved link id for deletion");
let status = unsafe { dladm_iptun_delete(handle.ptr, link_id, DLADM_OPT_ACTIVE) };
if status != DLADM_STATUS_OK {
return Err(TunnelError::DestroyFailed(format!(
"dladm_iptun_delete failed with status {}",
status
)));
}
Ok(())
}
fn delete_if(&self, handle: &IpadmHandle) -> Result<(), TunnelError> {
let status =
unsafe { ipadm_delete_if(handle.ptr, self.cname.as_ptr(), AF_INET, IPADM_OPT_ACTIVE) };
if status != IPADM_STATUS_OK {
return Err(TunnelError::DestroyFailed(format!(
"ipadm_delete_if failed with status {}",
status
)));
}
tracing::debug!("ip interface deleted");
Ok(())
}
fn rollback_setup(
&self,
dladm_handle: &DladmHandle,
interface_created: bool,
local_v4_configured: bool,
default_route_may_exist: bool,
) {
let mut failures = Vec::new();
if default_route_may_exist {
match RouteSocket::open().and_then(|route| route.delete_default_v4(AFTR_V4_ELEMENT)) {
Ok(()) => {}
Err(error) if error.raw_os_error() == Some(libc::ESRCH) => {}
Err(error) => failures.push(format!("removing default route: {error}")),
}
}
if local_v4_configured {
match open_inet_dgram_socket() {
Ok(socket) => {
if let Err(error) = unsafe {
set_local_addr(socket.as_raw_fd(), &self.cname, Ipv4Addr::UNSPECIFIED)
} {
failures.push(format!("clearing local IPv4 address: {error}"));
}
}
Err(error) => failures.push(format!(
"opening address configuration socket for rollback: {error}"
)),
}
}
if interface_created {
match open_ipadm() {
Ok(handle) => {
if let Err(error) = self.delete_if(&handle) {
failures.push(error.to_string());
}
}
Err(status) => failures.push(format!(
"opening libipadm handle for rollback failed with status {status}"
)),
}
}
if let Err(error) = self.delete_tunnel(dladm_handle) {
failures.push(error.to_string());
}
if failures.is_empty() {
tracing::debug!("rolled back partially created tunnel");
} else {
tracing::warn!(
errors = %failures.join("; "),
"failed to fully roll back partially created tunnel"
);
}
}
fn name_to_linkid(&self, handle: &DladmHandle) -> (u32, u32) {
let mut link_id: u32 = 0;
let status = unsafe {
dladm_name2info(
handle.ptr,
self.cname.as_ptr(),
&mut link_id,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
(link_id, status)
}
}
impl TunnelBackend for IllumosBackend {
async fn setup(&self, desired: DesiredState) -> Result<(), TunnelError> {
let handle = open_dladm().map_err(|e| {
TunnelError::CreationFailed(format!("unable to open handle, dladm_open status {}", e))
})?;
let link_id = self.create_tunnel(&handle, &desired)?;
let mut interface_created = false;
let mut local_v4_configured = false;
let mut default_route_may_exist = false;
let setup_result = (|| -> Result<(), TunnelError> {
if let Some(encapsulation_limit) = desired.encapsulation_limit {
self.set_encapsulation_limit(&handle, link_id, encapsulation_limit)
.map_err(|status| {
TunnelError::CreationFailed(format!(
"setting encaplimit, dladm_set_linkprop status: {status}"
))
})?;
}
let ip_handle = open_ipadm().map_err(|e| {
TunnelError::CreationFailed(format!("unable to open handle, ipadm_open status {e}"))
})?;
self.create_if(&ip_handle)?;
interface_created = true;
let sock_fd = open_inet_dgram_socket().map_err(|e| {
TunnelError::CreationFailed(format!("opening address configuration socket: {e}"))
})?;
let fd = sock_fd.as_raw_fd();
if let Some(mtu) = desired.mtu {
unsafe { set_mtu(fd, &self.cname, mtu) }
.map_err(|e| TunnelError::CreationFailed(format!("set_mtu: {e}")))?;
}
unsafe { set_local_addr(fd, &self.cname, desired.local_v4) }
.map_err(|e| TunnelError::CreationFailed(format!("set_local_addr: {e}")))?;
local_v4_configured = true;
unsafe { set_dst_addr(fd, &self.cname, AFTR_V4_ELEMENT) }
.map_err(|e| TunnelError::CreationFailed(format!("set_dst_addr: {e}")))?;
unsafe { set_netmask(fd, &self.cname, B4_V4_NETMASK) }
.map_err(|e| TunnelError::CreationFailed(format!("set_netmask: {e}")))?;
unsafe { bring_up(fd, &self.cname) }
.map_err(|e| TunnelError::CreationFailed(format!("bring_up: {e}")))?;
let route_sock = RouteSocket::open()
.map_err(|e| TunnelError::CreationFailed(format!("PF_ROUTE open: {e}")))?;
default_route_may_exist = true;
route_sock
.add_default_v4(AFTR_V4_ELEMENT)
.map_err(|e| TunnelError::CreationFailed(format!("add default route: {e}")))?;
Ok(())
})();
if let Err(error) = setup_result {
self.rollback_setup(
&handle,
interface_created,
local_v4_configured,
default_route_may_exist,
);
return Err(error);
}
tracing::info!(
name = %self.cname.to_string_lossy(),
local_v6 = %desired.local_v6,
remote_v6 = %desired.remote_v6,
local_v4 = %desired.local_v4,
mtu = ?desired.mtu,
encapsulation_limit = ?desired.encapsulation_limit,
"tunnel established"
);
Ok(())
}
async fn update(
&self,
_desired: DesiredState,
update: TunnelUpdate,
) -> Result<(), TunnelError> {
if let Some(encapsulation_limit) = update.encapsulation_limit {
let handle = open_dladm().map_err(|status| {
TunnelError::UpdateFailed(format!(
"opening libdladm handle, dladm_open status: {status}"
))
})?;
let (link_id, status) = self.name_to_linkid(&handle);
if status != DLADM_STATUS_OK {
return Err(TunnelError::UpdateFailed(format!(
"resolving link id, dladm_name2info status: {status}"
)));
}
self.set_encapsulation_limit(&handle, link_id, encapsulation_limit)
.map_err(|status| {
TunnelError::UpdateFailed(format!(
"setting encaplimit, dladm_set_linkprop status: {status}"
))
})?;
}
if update.mtu.is_some() || update.bring_up {
let socket = open_inet_dgram_socket().map_err(|e| {
TunnelError::UpdateFailed(format!("opening interface configuration socket: {e}"))
})?;
let fd = socket.as_raw_fd();
if let Some(mtu) = update.mtu {
unsafe { sys::set_mtu(fd, &self.cname, mtu) }.map_err(|e| {
TunnelError::UpdateFailed(format!("setting interface MTU: {e}"))
})?;
}
if update.bring_up {
unsafe { sys::bring_up(fd, &self.cname) }.map_err(|e| {
TunnelError::UpdateFailed(format!("setting interface flags: {e}"))
})?;
}
}
tracing::info!(
name = %self.cname.to_string_lossy(),
mtu = ?update.mtu,
encapsulation_limit = ?update.encapsulation_limit,
bring_up = update.bring_up,
"interface updated"
);
Ok(())
}
async fn teardown(&self) -> Result<(), TunnelError> {
let ip_handle = open_ipadm().map_err(|e| {
TunnelError::DestroyFailed(format!("unable to open handle, ipadm_open status {}", e))
})?;
let sock_fd = open_inet_dgram_socket().map_err(|e| {
TunnelError::DestroyFailed(format!("opening address configuration socket: {e}"))
})?;
let fd = sock_fd.as_raw_fd();
unsafe { set_local_addr(fd, &self.cname, Ipv4Addr::UNSPECIFIED) }
.map_err(|e| TunnelError::DestroyFailed(format!("zero local_v4: {}", e)))?;
let route_sock = RouteSocket::open()
.map_err(|e| TunnelError::DestroyFailed(format!("PF_ROUTE open: {e}")))?;
if let Err(e) = route_sock.delete_default_v4(AFTR_V4_ELEMENT) {
if e.raw_os_error() == Some(libc::ESRCH) {
tracing::warn!(error = %e, "default route already gone");
} else {
return Err(TunnelError::DestroyFailed(format!(
"delete default route: {e}"
)));
}
}
self.delete_if(&ip_handle)?;
let handle = open_dladm().map_err(|e| {
TunnelError::DestroyFailed(format!("unable to open handle, dladm_open status {}", e))
})?;
self.delete_tunnel(&handle)?;
tracing::info!(
name = %self.cname.to_string_lossy(),
"tunnel removed"
);
Ok(())
}
async fn observe(&self) -> Result<Observed, TunnelError> {
let handle = open_dladm().map_err(|e| {
TunnelError::StatusCheckFailed(format!(
"unable to open handle, dladm_open status {}",
e
))
})?;
let Some(params) = self.get_tunnel_params(&handle)? else {
return Ok(Observed::Absent);
};
if params.ip_tun_type != IPTUN_TYPE_IPV6 {
return Err(TunnelError::StatusCheckFailed(format!(
"expected IPv6 tunnel type, got {}",
params.ip_tun_type
)));
}
if params.flags & IPTUN_PARAM_LADDR == 0 {
return Err(TunnelError::StatusCheckFailed(
"tunnel local endpoint missing".to_string(),
));
}
if params.flags & IPTUN_PARAM_RADDR == 0 {
return Err(TunnelError::StatusCheckFailed(
"tunnel remote endpoint missing".to_string(),
));
}
let local_v6 = parse_tunnel_addr(¶ms.l_addr, "local")?;
let remote_v6 = parse_tunnel_addr(¶ms.r_addr, "remote")?;
let admin_up = self.is_admin_up()?;
let mtu = self.get_mtu()?;
let encapsulation_limit = self.get_encapsulation_limit(&handle, params.link_id)?;
Ok(Observed::Present {
local_v6,
remote_v6,
mtu,
encapsulation_limit,
admin_up,
})
}
}
struct DladmHandle {
ptr: *mut c_void,
}
impl Drop for DladmHandle {
fn drop(&mut self) {
unsafe { dladm_close(self.ptr) };
}
}
struct IpadmHandle {
ptr: *mut c_void,
}
impl Drop for IpadmHandle {
fn drop(&mut self) {
unsafe { ipadm_close(self.ptr) };
}
}
fn addr_to_caddr(addr: &std::net::IpAddr) -> [c_char; NI_MAXHOST] {
let s = addr.to_string();
let bytes = s.as_bytes();
let mut caddr = [0 as c_char; NI_MAXHOST];
for (i, &b) in bytes.iter().enumerate() {
caddr[i] = b as c_char;
}
caddr
}
fn build_tunnel_params(local: &Ipv6Addr, remote: &Ipv6Addr) -> IpTunParams {
IpTunParams {
link_id: 0,
flags: IPTUN_PARAM_TYPE | IPTUN_PARAM_LADDR | IPTUN_PARAM_RADDR,
ip_tun_type: IPTUN_TYPE_IPV6,
l_addr: addr_to_caddr(&IpAddr::V6(*local)),
r_addr: addr_to_caddr(&IpAddr::V6(*remote)),
sec_info: IpsecReq {
ipsr_ah_req: 0,
ipsr_esp_req: 0,
ipsr_self_encap_req: 0,
ipsr_auth_alg: 0,
ipsr_esp_alg: 0,
ipsr_esp_auth_alg: 0,
},
}
}
fn open_dladm() -> Result<DladmHandle, u32> {
let mut ptr: *mut c_void = std::ptr::null_mut();
let status = unsafe { dladm_open(&mut ptr) };
if status != DLADM_STATUS_OK {
return Err(status);
}
Ok(DladmHandle { ptr })
}
fn open_ipadm() -> Result<IpadmHandle, u32> {
let mut ptr: *mut c_void = std::ptr::null_mut();
let status = unsafe { ipadm_open(&mut ptr, 0) };
if status != IPADM_STATUS_OK {
return Err(status);
}
Ok(IpadmHandle { ptr })
}
fn open_inet_dgram_socket() -> io::Result<OwnedFd> {
let raw = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) };
if raw == -1 {
return Err(io::Error::last_os_error());
}
Ok(unsafe { OwnedFd::from_raw_fd(raw) })
}
fn parse_tunnel_addr(
value: &[c_char; NI_MAXHOST],
endpoint: &str,
) -> Result<Ipv6Addr, TunnelError> {
let nul = value.iter().position(|&byte| byte == 0).ok_or_else(|| {
TunnelError::StatusCheckFailed(format!("{endpoint} tunnel address is not NUL-terminated"))
})?;
let bytes: Vec<u8> = value[..nul].iter().map(|&byte| byte as u8).collect();
let text = str::from_utf8(&bytes).map_err(|e| {
TunnelError::StatusCheckFailed(format!("invalid {endpoint} tunnel address: {e}"))
})?;
text.parse::<Ipv6Addr>().map_err(|e| {
TunnelError::StatusCheckFailed(format!("invalid {endpoint} IPv6 address {text:?}: {e}"))
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::num::NonZeroU8;
fn test_desired_state() -> DesiredState {
DesiredState {
local_v6: Ipv6Addr::UNSPECIFIED,
remote_v6: Ipv6Addr::UNSPECIFIED,
local_v4: Ipv4Addr::UNSPECIFIED,
mtu: None,
encapsulation_limit: None,
}
}
fn test_encapsulation_limit_value() -> u8 {
std::env::var("DSLITE_TEST_ENCAP_LIMIT")
.expect("DSLITE_TEST_ENCAP_LIMIT must contain the prepared encapsulation limit")
.parse()
.expect("DSLITE_TEST_ENCAP_LIMIT must be an integer from 0 through 255")
}
fn c_addr(value: &str) -> [c_char; NI_MAXHOST] {
let mut result = [0; NI_MAXHOST];
for (destination, source) in result.iter_mut().zip(value.bytes()) {
*destination = source as c_char;
}
result
}
#[test]
fn parses_ipv6_tunnel_address() {
let value = c_addr("2001:db8::1");
let address = parse_tunnel_addr(&value, "local").unwrap();
assert_eq!(address, "2001:db8::1".parse::<Ipv6Addr>().unwrap());
}
#[test]
fn rejects_invalid_ipv6_tunnel_address() {
let value = c_addr("not-an-address");
let error = parse_tunnel_addr(&value, "remote").unwrap_err();
assert!(error.to_string().contains("invalid remote IPv6 address"));
}
#[test]
fn rejects_non_terminated_tunnel_address() {
let value = [b'a' as c_char; NI_MAXHOST];
let error = parse_tunnel_addr(&value, "local").unwrap_err();
assert!(
error
.to_string()
.contains("local tunnel address is not NUL-terminated")
);
}
#[tokio::test]
#[ignore = "requires tunnel state prepared by crates/dslite-b4/scripts/test-illumos-observe.sh"]
async fn sets_illumos_tunnel_mtu() {
let name = std::env::var("DSLITE_TEST_TUNNEL")
.expect("DSLITE_TEST_TUNNEL must name the prepared test tunnel");
let mtu = std::env::var("DSLITE_TEST_MTU")
.expect("DSLITE_TEST_MTU must contain the requested tunnel MTU")
.parse()
.expect("DSLITE_TEST_MTU must be an unsigned integer");
let backend = IllumosBackend::new(name).unwrap();
backend
.update(
test_desired_state(),
TunnelUpdate {
mtu: Some(mtu),
encapsulation_limit: None,
bring_up: false,
},
)
.await
.unwrap();
assert_eq!(backend.get_mtu().unwrap(), mtu);
}
#[tokio::test]
#[ignore = "requires tunnel state prepared by crates/dslite-b4/scripts/test-illumos-observe.sh"]
async fn observes_illumos_tunnel() {
let name = std::env::var("DSLITE_TEST_TUNNEL")
.expect("DSLITE_TEST_TUNNEL must name the prepared test tunnel");
let expected = std::env::var("DSLITE_TEST_EXPECT")
.expect("DSLITE_TEST_EXPECT must be present-up, present-down, or absent");
let backend = IllumosBackend::new(name).unwrap();
let observed = backend.observe().await.unwrap();
if expected == "absent" {
assert_eq!(observed, Observed::Absent);
return;
}
let local_v6 = std::env::var("DSLITE_TEST_LOCAL_V6")
.expect("DSLITE_TEST_LOCAL_V6 must contain the prepared local endpoint")
.parse()
.expect("DSLITE_TEST_LOCAL_V6 must be an IPv6 address");
let remote_v6 = std::env::var("DSLITE_TEST_REMOTE_V6")
.expect("DSLITE_TEST_REMOTE_V6 must contain the prepared remote endpoint")
.parse()
.expect("DSLITE_TEST_REMOTE_V6 must be an IPv6 address");
let mtu = std::env::var("DSLITE_TEST_MTU")
.expect("DSLITE_TEST_MTU must contain the prepared tunnel MTU")
.parse()
.expect("DSLITE_TEST_MTU must be an unsigned integer");
let encapsulation_limit = test_encapsulation_limit_value();
let admin_up = match expected.as_str() {
"present-up" => true,
"present-down" => false,
value => panic!("unexpected DSLITE_TEST_EXPECT value: {value}"),
};
assert_eq!(
observed,
Observed::Present {
local_v6,
remote_v6,
mtu,
encapsulation_limit: (encapsulation_limit != 0).then_some(encapsulation_limit),
admin_up,
}
);
}
#[tokio::test]
#[ignore = "requires tunnel state prepared by crates/dslite-b4/scripts/test-illumos-observe.sh"]
async fn sets_illumos_tunnel_encapsulation_limit() {
let name = std::env::var("DSLITE_TEST_TUNNEL")
.expect("DSLITE_TEST_TUNNEL must name the prepared test tunnel");
let value = test_encapsulation_limit_value();
let configured = match NonZeroU8::new(value) {
Some(value) => EncapsulationLimit::Value(value),
None => EncapsulationLimit::Disabled,
};
let backend = IllumosBackend::new(name).unwrap();
backend
.update(
test_desired_state(),
TunnelUpdate {
mtu: None,
encapsulation_limit: Some(configured),
bring_up: false,
},
)
.await
.unwrap();
let Observed::Present {
encapsulation_limit,
..
} = backend.observe().await.unwrap()
else {
panic!("prepared tunnel is absent");
};
assert_eq!(encapsulation_limit, (value != 0).then_some(value),);
}
#[tokio::test]
#[ignore = "requires tunnel state prepared by crates/dslite-b4/scripts/test-illumos-observe.sh"]
async fn brings_up_illumos_tunnel() {
let name = std::env::var("DSLITE_TEST_TUNNEL")
.expect("DSLITE_TEST_TUNNEL must name the prepared test tunnel");
let local_v6 = std::env::var("DSLITE_TEST_LOCAL_V6")
.expect("DSLITE_TEST_LOCAL_V6 must contain the prepared local endpoint")
.parse()
.expect("DSLITE_TEST_LOCAL_V6 must be an IPv6 address");
let remote_v6 = std::env::var("DSLITE_TEST_REMOTE_V6")
.expect("DSLITE_TEST_REMOTE_V6 must contain the prepared remote endpoint")
.parse()
.expect("DSLITE_TEST_REMOTE_V6 must be an IPv6 address");
let mtu = std::env::var("DSLITE_TEST_MTU")
.expect("DSLITE_TEST_MTU must contain the prepared tunnel MTU")
.parse()
.expect("DSLITE_TEST_MTU must be an unsigned integer");
let encapsulation_limit = test_encapsulation_limit_value();
let backend = IllumosBackend::new(name).unwrap();
backend
.update(
test_desired_state(),
TunnelUpdate {
mtu: None,
encapsulation_limit: None,
bring_up: true,
},
)
.await
.unwrap();
assert_eq!(
backend.observe().await.unwrap(),
Observed::Present {
local_v6,
remote_v6,
mtu,
encapsulation_limit: (encapsulation_limit != 0).then_some(encapsulation_limit),
admin_up: true,
}
);
}
}