use crate::config::types::{MachineConfig, OutboundLinkConfig};
use crate::link::LinkHost;
use crate::link_carrier::{CarrierError, WireCarrier, read_frame, write_frame};
use crate::link_handshake::{LinkHandshake, LinkHandshakeError, LinkTransport, NOISE_OVERHEAD};
use liminal::protocol::{
Frame, LinkEnd, LinkOffer, LinkOpenOutcome, ProtocolVersion, REVOCATION_CLOSE_VERSION,
};
use liminal_protocol::credential::WireCredential;
use std::net::{TcpStream, ToSocketAddrs};
#[derive(Debug)]
pub struct DialedLink {
pub link_id: String,
pub exports: Vec<String>,
pub transport: LinkTransport,
pub carrier: WireCarrier,
}
#[derive(Debug)]
pub enum LinkDialOutcome {
Opened(Box<DialedLink>),
Held { grant: String, draft: String },
Refused { reason: String },
}
#[derive(Debug, thiserror::Error)]
pub enum LinkDialError {
#[error("outbound link configuration refused: {0}")]
Configuration(String),
#[error("outbound link I/O failed: {0}")]
Io(#[from] std::io::Error),
#[error(transparent)]
Carrier(#[from] CarrierError),
#[error(transparent)]
Handshake(#[from] LinkHandshakeError),
#[error("peer refused Connect ({reason_code}): {reason:?}")]
ConnectRefused {
reason_code: u16,
reason: Option<String>,
},
#[error("peer selected an unoffered protocol version")]
Version,
#[error("wire ended before {0}")]
Ended(&'static str),
}
pub struct LinkDialer<'a> {
host: &'a LinkHost,
agent_id: String,
network: Option<&'a crate::link_open::network::Network>,
pass: Result<crate::link_pass::MachinePass, crate::link_pass::PassRefusal>,
topology:
Option<std::sync::Arc<crate::server::connection::services::LiminalConnectionServices>>,
}
impl std::fmt::Debug for LinkDialer<'_> {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("LinkDialer")
.field("host", &self.host)
.field(
"pass",
&self.pass.as_ref().map(crate::link_pass::debug::PassLength),
)
.field("topology", &self.topology)
.finish()
}
}
impl<'a> LinkDialer<'a> {
#[must_use]
pub fn new(host: &'a LinkHost, machine: &MachineConfig) -> Self {
Self {
host,
agent_id: machine.agent_id.clone(),
pass: crate::link_pass::MachinePass::configured(machine),
topology: None,
network: None,
}
}
pub(crate) const fn with_network(
mut self,
network: &'a crate::link_open::network::Network,
) -> Self {
self.network = Some(network);
self
}
#[must_use]
pub fn with_topology(
mut self,
services: std::sync::Arc<crate::server::connection::services::LiminalConnectionServices>,
) -> Self {
self.topology = Some(services);
self
}
pub fn open(&self, entry: &OutboundLinkConfig) -> Result<LinkDialOutcome, LinkDialError> {
let address = entry.peer_address.as_deref().ok_or_else(|| {
LinkDialError::Configuration(format!("link '{}' has no peer_address", entry.link_id))
})?;
let mut handshake = self.handshake(entry)?;
let mut first = vec![0; NOISE_OVERHEAD];
let length = handshake.write(&mut first)?;
first.truncate(length);
let first = self.certificate_offer(&first)?;
let (mut socket, interruption) = self.connect(address)?;
let stream_id = 1;
write_frame(
&mut socket,
&Frame::LinkOpen {
flags: 0,
stream_id,
offer: LinkOffer {
link_id: entry.link_id.clone(),
end: LinkEnd::Export,
machine: *self.host.local().machine().ed25519_public(),
handshake: first,
},
},
)?;
let outcome = match read_frame(&mut socket)? {
Some(Frame::LinkOpenAck {
stream_id: answered_stream,
outcome,
..
}) if answered_stream == stream_id => outcome,
None => return Err(LinkDialError::Ended("LinkOpenAck")),
Some(_) => return Err(CarrierError::UnexpectedFrame.into()),
};
socket.set_read_timeout(None)?;
socket.set_write_timeout(None)?;
drop(interruption);
match outcome {
LinkOpenOutcome::Opened { handshake: answer } => {
handshake.read(&answer)?;
let transport = handshake.finish()?;
let mut carrier = WireCarrier::new(socket, stream_id)?;
let life = crate::link::LinkLife::new(*transport.peer());
if let Some(services) = &self.topology {
let local = services.console().device().to_owned();
let services = std::sync::Arc::clone(services);
life.observe(
local,
std::sync::Arc::new(move |change| {
if let Err(error) = services.console().emit(change) {
tracing::error!(%error, "dialed link topology failed");
}
}),
)
.map_err(LinkDialError::Configuration)?;
}
carrier.install_life(life)?;
Ok(LinkDialOutcome::Opened(Box::new(DialedLink {
link_id: entry.link_id.clone(),
exports: entry.exports.clone(),
transport,
carrier,
})))
}
LinkOpenOutcome::Held { grant, draft } => Ok(LinkDialOutcome::Held { grant, draft }),
LinkOpenOutcome::Refused { reason } => Ok(LinkDialOutcome::Refused { reason }),
}
}
fn handshake(&self, entry: &OutboundLinkConfig) -> Result<LinkHandshake, LinkDialError> {
if entry.link_id.is_empty()
|| entry
.link_id
.chars()
.any(|c| c.is_whitespace() || c.is_control())
{
return Err(LinkDialError::Configuration(
"link_id is not an identifier".to_owned(),
));
}
let peer = self
.host
.recorded_machine(&entry.peer_holder)
.ok_or_else(|| {
LinkDialError::Configuration(format!(
"peer_holder '{}' has no recorded machine key",
entry.peer_holder
))
})?;
for subject in &entry.exports {
crate::auth_lys::SubjectBinding::new(
"wire".to_owned(),
subject.clone(),
subject.clone(),
)
.map_err(|error| LinkDialError::Configuration(error.to_string()))?;
}
Ok(LinkHandshake::initiator(self.host.local(), peer)?)
}
fn connect(
&self,
address: &str,
) -> Result<(TcpStream, Option<std::sync::Arc<TcpStream>>), LinkDialError> {
let pass = self
.pass
.as_ref()
.map_err(|error| LinkDialError::Configuration(error.to_string()))?;
let token = pass
.for_open()
.map_err(|error| LinkDialError::Configuration(error.to_string()))?;
let token = token.trim();
if token.is_empty() {
return Err(LinkDialError::Configuration(
"machine pass is empty".to_owned(),
));
}
let credential = WireCredential::lys_record(token.as_bytes());
if credential.len() > crate::server::connection::pre_admission::MAX_CONNECT_AUTH_TOKEN_BYTES
{
return Err(LinkDialError::Configuration(
"machine pass exceeds the Connect bound".to_owned(),
));
}
let (mut socket, interruption) = if let Some(network) = self.network {
let (socket, interruption) = network
.wire_socket(address)
.map_err(|error| CarrierError::Lifecycle(error.0))?;
(socket, Some(interruption))
} else {
let target = address.to_socket_addrs()?.next().ok_or_else(|| {
LinkDialError::Configuration(
"peer_address resolved to no socket address".to_owned(),
)
})?;
(TcpStream::connect(target)?, None)
};
socket.set_nodelay(true)?;
write_frame(
&mut socket,
&Frame::Connect {
flags: 0,
min_version: ProtocolVersion::new(1, 0),
max_version: REVOCATION_CLOSE_VERSION,
auth_token: credential,
},
)?;
match read_frame(&mut socket)? {
Some(Frame::ConnectAck {
selected_version, ..
}) => {
if selected_version < ProtocolVersion::new(1, 0)
|| selected_version > REVOCATION_CLOSE_VERSION
{
return Err(LinkDialError::Version);
}
}
Some(Frame::ConnectError {
reason_code,
message,
..
}) => {
return Err(LinkDialError::ConnectRefused {
reason_code,
reason: message,
});
}
None => return Err(LinkDialError::Ended("ConnectAck")),
Some(_) => return Err(CarrierError::UnexpectedFrame.into()),
}
Ok((socket, interruption))
}
}
#[path = "link_dial/certificate.rs"]
mod certificate;