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