1use core::time::Duration;
13use std::fmt;
14
15use bytes::Bytes;
16use ts_capabilityversion::CapabilityVersion;
17use ts_control_serde::{SetDnsRequest, SetDnsResponse};
18use ts_http_util::{BytesBody, ClientExt, Http2, ResponseExt, StatusCode};
19use url::Url;
20
21use crate::tokio::connect::ConnectionError;
22
23const LOAD_BALANCER_HEADER_KEY: &str = "Ts-Lb";
24
25const SET_DNS_TIMEOUT: Duration = Duration::from_secs(30);
30
31#[derive(Debug, Clone, Copy, Eq, PartialEq)]
36pub enum SetDnsInternalErrorKind {
37 Url,
39 SerDe,
41 Http,
43 Utf8,
45}
46
47impl fmt::Display for SetDnsInternalErrorKind {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 match self {
50 SetDnsInternalErrorKind::Url => write!(f, "URL parsing error"),
51 SetDnsInternalErrorKind::SerDe => write!(f, "serialization/deserialization error"),
52 SetDnsInternalErrorKind::Http => write!(f, "unsuccessful HTTP request"),
53 SetDnsInternalErrorKind::Utf8 => write!(f, "invalid UTF8"),
54 }
55 }
56}
57
58#[derive(Debug, thiserror::Error, Clone, Eq, PartialEq)]
60pub enum SetDnsError {
61 #[error("network error publishing dns record")]
63 NetworkError,
64 #[error("error publishing dns record: {0}")]
66 Internal(SetDnsInternalErrorKind),
67}
68
69impl From<url::ParseError> for SetDnsError {
70 fn from(error: url::ParseError) -> Self {
71 tracing::error!(%error, "bad URL building set-dns request");
72 SetDnsError::Internal(SetDnsInternalErrorKind::Url)
73 }
74}
75
76impl From<serde_json::Error> for SetDnsError {
77 fn from(error: serde_json::Error) -> Self {
78 tracing::error!(%error, "serde error in set-dns request");
79 SetDnsError::Internal(SetDnsInternalErrorKind::SerDe)
80 }
81}
82
83impl From<core::str::Utf8Error> for SetDnsError {
84 fn from(error: core::str::Utf8Error) -> Self {
85 tracing::error!(%error, "invalid utf8 in set-dns response");
86 SetDnsError::Internal(SetDnsInternalErrorKind::Utf8)
87 }
88}
89
90impl From<ts_http_util::Error> for SetDnsError {
91 fn from(error: ts_http_util::Error) -> Self {
92 tracing::error!(%error, "http error in set-dns request");
93 if crate::http_error_is_recoverable(error) {
94 SetDnsError::NetworkError
95 } else {
96 SetDnsError::Internal(SetDnsInternalErrorKind::Http)
97 }
98 }
99}
100
101impl From<ConnectionError> for SetDnsError {
104 fn from(error: ConnectionError) -> Self {
105 use crate::tokio::connect::InternalErrorKind as Conn;
106 match error {
107 ConnectionError::NetworkError => SetDnsError::NetworkError,
108 ConnectionError::Internal(k) => SetDnsError::Internal(match k {
109 Conn::Url => SetDnsInternalErrorKind::Url,
110 Conn::SerDe => SetDnsInternalErrorKind::SerDe,
111 Conn::Http
113 | Conn::MessageFormat
114 | Conn::Io
115 | Conn::ChallengeLength
116 | Conn::NoiseHandshake => SetDnsInternalErrorKind::Http,
117 }),
118 }
119 }
120}
121
122pub async fn set_dns(
133 config: &crate::Config,
134 node_keystate: &ts_keys::NodeState,
135 name: &str,
136 record_type: &str,
137 value: &str,
138) -> Result<(), SetDnsError> {
139 let control_url = &config.server_url;
140 let rpc = async {
141 let http2_conn = crate::tokio::connect(
142 control_url,
143 &node_keystate.machine_keys,
144 config.allow_http_key_fetch,
145 )
146 .await?;
147 set_dns_with(
148 control_url,
149 node_keystate,
150 name,
151 record_type,
152 value,
153 &http2_conn,
154 )
155 .await
156 };
157
158 match tokio::time::timeout(SET_DNS_TIMEOUT, rpc).await {
159 Ok(result) => result,
160 Err(_elapsed) => {
161 tracing::error!(timeout = ?SET_DNS_TIMEOUT, "set-dns request timed out");
162 Err(SetDnsError::NetworkError)
163 }
164 }
165}
166
167pub(crate) async fn set_dns_with(
172 control_url: &Url,
173 node_keystate: &ts_keys::NodeState,
174 name: &str,
175 record_type: &str,
176 value: &str,
177 http2_conn: &Http2<BytesBody>,
178) -> Result<(), SetDnsError> {
179 let node_public_key = node_keystate.node_keys.public;
180
181 let req = SetDnsRequest {
182 version: CapabilityVersion::CURRENT,
183 node_key: node_public_key,
184 name: name.to_string(),
185 r#type: record_type.to_string(),
186 value: value.to_string(),
187 };
188
189 let body = serde_json::to_string(&req)?;
190 let url = control_url.join("machine/set-dns")?;
191
192 tracing::debug!(url = %url.as_str(), name, record_type, "publishing dns record via control");
193
194 let response = http2_conn
195 .post(
196 &url,
197 [(
198 LOAD_BALANCER_HEADER_KEY.parse().unwrap(),
199 node_public_key.to_string().parse().unwrap(),
200 )],
201 Bytes::from(body).into(),
202 )
203 .await?;
204
205 let status = response.status();
206 let body = response.collect_bytes().await?;
207 check_set_dns_status(status, &body)
208}
209
210fn check_set_dns_status(status: StatusCode, body: &[u8]) -> Result<(), SetDnsError> {
218 if !status.is_success() {
219 let mut truncated = body.to_vec();
220 truncated.truncate(512);
221 let preview = core::str::from_utf8(&truncated).unwrap_or("<invalid utf8>");
222 tracing::error!(body = %preview, %status, "set-dns request failed");
223 return Err(SetDnsError::Internal(SetDnsInternalErrorKind::Http));
224 }
225
226 let body = core::str::from_utf8(body)?;
227 if body.trim().is_empty() {
229 return Ok(());
230 }
231 let _resp: SetDnsResponse = serde_json::from_str(body)?;
233 Ok(())
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use crate::tokio::connect::{ConnectionError, InternalErrorKind as ConnKind};
240
241 #[test]
244 fn connection_error_network_maps_to_network() {
245 assert_eq!(
246 SetDnsError::from(ConnectionError::NetworkError),
247 SetDnsError::NetworkError
248 );
249 }
250
251 #[test]
252 fn connection_error_internal_kinds_map_correctly() {
253 use SetDnsInternalErrorKind as Sd;
254 let cases = [
255 (ConnKind::Url, Sd::Url),
256 (ConnKind::SerDe, Sd::SerDe),
257 (ConnKind::Http, Sd::Http),
258 (ConnKind::MessageFormat, Sd::Http),
259 (ConnKind::Io, Sd::Http),
260 (ConnKind::ChallengeLength, Sd::Http),
261 (ConnKind::NoiseHandshake, Sd::Http),
262 ];
263 for (conn, expected) in cases {
264 assert_eq!(
265 SetDnsError::from(ConnectionError::Internal(conn)),
266 SetDnsError::Internal(expected),
267 "ConnectionError::Internal({conn:?}) should map to Internal({expected:?})"
268 );
269 }
270 }
271
272 #[test]
273 fn serde_error_maps_to_internal_serde() {
274 let err = serde_json::from_str::<SetDnsResponse>("not json").unwrap_err();
275 assert_eq!(
276 SetDnsError::from(err),
277 SetDnsError::Internal(SetDnsInternalErrorKind::SerDe)
278 );
279 }
280
281 #[test]
282 fn url_parse_error_maps_to_internal_url() {
283 let err = Url::parse("not a url").unwrap_err();
284 assert_eq!(
285 SetDnsError::from(err),
286 SetDnsError::Internal(SetDnsInternalErrorKind::Url)
287 );
288 }
289
290 #[test]
291 fn utf8_error_maps_to_internal_utf8() {
292 let bytes = vec![0xffu8, 0xfe];
295 let err = core::str::from_utf8(&bytes).unwrap_err();
296 assert_eq!(
297 SetDnsError::from(err),
298 SetDnsError::Internal(SetDnsInternalErrorKind::Utf8)
299 );
300 }
301
302 #[test]
303 fn http_util_error_non_recoverable_maps_to_internal_http() {
304 let err = ts_http_util::Error::InvalidResponse;
305 assert_eq!(
306 SetDnsError::from(err),
307 SetDnsError::Internal(SetDnsInternalErrorKind::Http)
308 );
309 }
310
311 #[test]
312 fn http_util_error_recoverable_maps_to_network() {
313 let err = ts_http_util::Error::Io;
314 assert_eq!(SetDnsError::from(err), SetDnsError::NetworkError);
315 }
316
317 #[test]
320 fn check_set_dns_status_ok_empty_body() {
321 check_set_dns_status(StatusCode::OK, b"").unwrap();
323 }
324
325 #[test]
326 fn check_set_dns_status_ok_empty_json() {
327 check_set_dns_status(StatusCode::OK, b"{}").unwrap();
329 }
330
331 #[test]
332 fn check_set_dns_status_self_hosted_501_is_error() {
333 let err =
336 check_set_dns_status(StatusCode::NOT_IMPLEMENTED, b"not implemented").unwrap_err();
337 assert_eq!(err, SetDnsError::Internal(SetDnsInternalErrorKind::Http));
338 }
339
340 #[test]
341 fn check_set_dns_status_500_is_error() {
342 let err =
343 check_set_dns_status(StatusCode::INTERNAL_SERVER_ERROR, b"upstream boom").unwrap_err();
344 assert_eq!(err, SetDnsError::Internal(SetDnsInternalErrorKind::Http));
345 }
346}