1use crate::config::types::{MachineConfig, OutboundLinkConfig};
4use crate::link::LinkHost;
5use crate::link_carrier::{CarrierError, WireCarrier, read_frame, write_frame};
6use crate::link_handshake::{LinkHandshake, LinkHandshakeError, LinkTransport, NOISE_OVERHEAD};
7use liminal::protocol::{
8 Frame, LinkEnd, LinkOffer, LinkOpenOutcome, ProtocolVersion, REVOCATION_CLOSE_VERSION,
9};
10use liminal_protocol::credential::WireCredential;
11use std::net::{TcpStream, ToSocketAddrs};
12
13#[derive(Debug)]
15pub struct DialedLink {
16 pub link_id: String,
18 pub exports: Vec<String>,
20 pub transport: LinkTransport,
22 pub carrier: WireCarrier,
24}
25
26#[derive(Debug)]
28pub enum LinkDialOutcome {
29 Opened(Box<DialedLink>),
31 Held { grant: String, draft: String },
33 Refused { reason: String },
35}
36
37#[derive(Debug, thiserror::Error)]
39pub enum LinkDialError {
40 #[error("outbound link configuration refused: {0}")]
42 Configuration(String),
43 #[error("outbound link I/O failed: {0}")]
45 Io(#[from] std::io::Error),
46 #[error(transparent)]
48 Carrier(#[from] CarrierError),
49 #[error(transparent)]
51 Handshake(#[from] LinkHandshakeError),
52 #[error("peer refused Connect ({reason_code}): {reason:?}")]
54 ConnectRefused {
55 reason_code: u16,
56 reason: Option<String>,
57 },
58 #[error("peer selected an unoffered protocol version")]
60 Version,
61 #[error("wire ended before {0}")]
63 Ended(&'static str),
64}
65
66pub struct LinkDialer<'a> {
68 host: &'a LinkHost,
69 agent_id: String,
70 network: Option<&'a crate::link_open::network::Network>,
71 pass: Result<crate::link_pass::MachinePass, crate::link_pass::PassRefusal>,
72 topology:
73 Option<std::sync::Arc<crate::server::connection::services::LiminalConnectionServices>>,
74}
75
76impl std::fmt::Debug for LinkDialer<'_> {
77 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 formatter
79 .debug_struct("LinkDialer")
80 .field("host", &self.host)
81 .field(
82 "pass",
83 &self.pass.as_ref().map(crate::link_pass::debug::PassLength),
84 )
85 .field("topology", &self.topology)
86 .finish()
87 }
88}
89
90impl<'a> LinkDialer<'a> {
91 #[must_use]
93 pub fn new(host: &'a LinkHost, machine: &MachineConfig) -> Self {
94 Self {
95 host,
96 agent_id: machine.agent_id.clone(),
97 pass: crate::link_pass::MachinePass::configured(machine),
98 topology: None,
99 network: None,
100 }
101 }
102
103 pub(crate) const fn with_network(
104 mut self,
105 network: &'a crate::link_open::network::Network,
106 ) -> Self {
107 self.network = Some(network);
108 self
109 }
110
111 #[must_use]
113 pub fn with_topology(
114 mut self,
115 services: std::sync::Arc<crate::server::connection::services::LiminalConnectionServices>,
116 ) -> Self {
117 self.topology = Some(services);
118 self
119 }
120
121 pub fn open(&self, entry: &OutboundLinkConfig) -> Result<LinkDialOutcome, LinkDialError> {
130 let address = entry.peer_address.as_deref().ok_or_else(|| {
131 LinkDialError::Configuration(format!("link '{}' has no peer_address", entry.link_id))
132 })?;
133 let mut handshake = self.handshake(entry)?;
134 let mut first = vec![0; NOISE_OVERHEAD];
135 let length = handshake.write(&mut first)?;
136 first.truncate(length);
137 let first = self.certificate_offer(&first)?;
138 let (mut socket, interruption) = self.connect(address)?;
139 let stream_id = 1;
140 write_frame(
141 &mut socket,
142 &Frame::LinkOpen {
143 flags: 0,
144 stream_id,
145 offer: LinkOffer {
146 link_id: entry.link_id.clone(),
147 end: LinkEnd::Export,
148 machine: *self.host.local().machine().ed25519_public(),
149 handshake: first,
150 },
151 },
152 )?;
153 let outcome = match read_frame(&mut socket)? {
154 Some(Frame::LinkOpenAck {
155 stream_id: answered_stream,
156 outcome,
157 ..
158 }) if answered_stream == stream_id => outcome,
159 None => return Err(LinkDialError::Ended("LinkOpenAck")),
160 Some(_) => return Err(CarrierError::UnexpectedFrame.into()),
161 };
162 socket.set_read_timeout(None)?;
163 socket.set_write_timeout(None)?;
164 drop(interruption);
165 match outcome {
166 LinkOpenOutcome::Opened { handshake: answer } => {
167 handshake.read(&answer)?;
168 let transport = handshake.finish()?;
169 let mut carrier = WireCarrier::new(socket, stream_id)?;
170 let life = crate::link::LinkLife::new(*transport.peer());
171 if let Some(services) = &self.topology {
172 let local = services.console().device().to_owned();
173 let services = std::sync::Arc::clone(services);
174 life.observe(
175 local,
176 std::sync::Arc::new(move |change| {
177 if let Err(error) = services.console().emit(change) {
178 tracing::error!(%error, "dialed link topology failed");
179 }
180 }),
181 )
182 .map_err(LinkDialError::Configuration)?;
183 }
184 carrier.install_life(life)?;
185 Ok(LinkDialOutcome::Opened(Box::new(DialedLink {
186 link_id: entry.link_id.clone(),
187 exports: entry.exports.clone(),
188 transport,
189 carrier,
190 })))
191 }
192 LinkOpenOutcome::Held { grant, draft } => Ok(LinkDialOutcome::Held { grant, draft }),
193 LinkOpenOutcome::Refused { reason } => Ok(LinkDialOutcome::Refused { reason }),
194 }
195 }
196
197 fn handshake(&self, entry: &OutboundLinkConfig) -> Result<LinkHandshake, LinkDialError> {
198 if entry.link_id.is_empty()
199 || entry
200 .link_id
201 .chars()
202 .any(|c| c.is_whitespace() || c.is_control())
203 {
204 return Err(LinkDialError::Configuration(
205 "link_id is not an identifier".to_owned(),
206 ));
207 }
208 let peer = self
209 .host
210 .recorded_machine(&entry.peer_holder)
211 .ok_or_else(|| {
212 LinkDialError::Configuration(format!(
213 "peer_holder '{}' has no recorded machine key",
214 entry.peer_holder
215 ))
216 })?;
217 for subject in &entry.exports {
218 crate::auth_lys::SubjectBinding::new(
219 "wire".to_owned(),
220 subject.clone(),
221 subject.clone(),
222 )
223 .map_err(|error| LinkDialError::Configuration(error.to_string()))?;
224 }
225 Ok(LinkHandshake::initiator(self.host.local(), peer)?)
226 }
227
228 fn connect(
229 &self,
230 address: &str,
231 ) -> Result<(TcpStream, Option<std::sync::Arc<TcpStream>>), LinkDialError> {
232 let pass = self
233 .pass
234 .as_ref()
235 .map_err(|error| LinkDialError::Configuration(error.to_string()))?;
236 let token = pass
237 .for_open()
238 .map_err(|error| LinkDialError::Configuration(error.to_string()))?;
239 let token = token.trim();
240 if token.is_empty() {
241 return Err(LinkDialError::Configuration(
242 "machine pass is empty".to_owned(),
243 ));
244 }
245 let credential = WireCredential::lys_record(token.as_bytes());
246 if credential.len() > crate::server::connection::pre_admission::MAX_CONNECT_AUTH_TOKEN_BYTES
247 {
248 return Err(LinkDialError::Configuration(
249 "machine pass exceeds the Connect bound".to_owned(),
250 ));
251 }
252 let (mut socket, interruption) = if let Some(network) = self.network {
253 let (socket, interruption) = network
254 .wire_socket(address)
255 .map_err(|error| CarrierError::Lifecycle(error.0))?;
256 (socket, Some(interruption))
257 } else {
258 let target = address.to_socket_addrs()?.next().ok_or_else(|| {
259 LinkDialError::Configuration(
260 "peer_address resolved to no socket address".to_owned(),
261 )
262 })?;
263 (TcpStream::connect(target)?, None)
264 };
265 socket.set_nodelay(true)?;
266 write_frame(
267 &mut socket,
268 &Frame::Connect {
269 flags: 0,
270 min_version: ProtocolVersion::new(1, 0),
271 max_version: REVOCATION_CLOSE_VERSION,
272 auth_token: credential,
273 },
274 )?;
275 match read_frame(&mut socket)? {
276 Some(Frame::ConnectAck {
277 selected_version, ..
278 }) => {
279 if selected_version < ProtocolVersion::new(1, 0)
280 || selected_version > REVOCATION_CLOSE_VERSION
281 {
282 return Err(LinkDialError::Version);
283 }
284 }
285 Some(Frame::ConnectError {
286 reason_code,
287 message,
288 ..
289 }) => {
290 return Err(LinkDialError::ConnectRefused {
291 reason_code,
292 reason: message,
293 });
294 }
295 None => return Err(LinkDialError::Ended("ConnectAck")),
296 Some(_) => return Err(CarrierError::UnexpectedFrame.into()),
297 }
298 Ok((socket, interruption))
299 }
300}
301
302#[path = "link_dial/certificate.rs"]
303mod certificate;