Skip to main content

shared/
tunnel.rs

1use crate::{database::Database, prelude::*};
2use ed25519_dalek::pkcs8::EncodePrivateKey;
3use serde::{Deserialize, Serialize};
4use sqlx::{Row, Type};
5use std::sync::{Arc, OnceLock};
6use tundra_common::{
7    hash::Hash32,
8    jwt::JwtIssuer,
9    state::{AclEntry, NodeEntry, PortSpec, Proto, ServerEntry, Snapshot},
10};
11use utoipa::ToSchema;
12
13pub use tundra_common::state::MAX_SERVER_IDX;
14
15const JWT_KEY_SETTING: &str = "::tunnel_jwt_key";
16
17/// Every write that changes what [`snapshot`] would return has to bump the epoch, or the nodes
18/// keep serving the state they already hold. Models bump; the route that owns the commit then
19/// calls [`poke_nodes`].
20pub async fn bump_epoch<'a>(
21    executor: impl sqlx::PgExecutor<'a>,
22) -> Result<i64, crate::database::DatabaseError> {
23    Ok(sqlx::query_scalar("SELECT nextval('tunnel_epoch')")
24        .fetch_one(executor)
25        .await?)
26}
27
28pub async fn bump_epoch_if_node_on_mesh(
29    transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
30    node_uuid: uuid::Uuid,
31) -> Result<bool, crate::database::DatabaseError> {
32    let member: bool = sqlx::query_scalar(
33        "SELECT EXISTS (SELECT 1 FROM node_tunnels WHERE node_tunnels.node_uuid = $1)",
34    )
35    .bind(node_uuid)
36    .fetch_one(&mut **transaction)
37    .await?;
38
39    if member {
40        bump_epoch(&mut **transaction).await?;
41    }
42
43    Ok(member)
44}
45
46pub async fn bump_epoch_if_server_on_mesh(
47    transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
48    server_uuid: uuid::Uuid,
49) -> Result<bool, crate::database::DatabaseError> {
50    let member: bool = sqlx::query_scalar(
51        "SELECT EXISTS (SELECT 1 FROM server_tunnels WHERE server_tunnels.server_uuid = $1)",
52    )
53    .bind(server_uuid)
54    .fetch_one(&mut **transaction)
55    .await?;
56
57    if member {
58        bump_epoch(&mut **transaction).await?;
59    }
60
61    Ok(member)
62}
63
64pub async fn epoch<'a>(
65    executor: impl sqlx::PgExecutor<'a>,
66) -> Result<i64, crate::database::DatabaseError> {
67    Ok(sqlx::query_scalar("SELECT last_value FROM tunnel_epoch")
68        .fetch_one(executor)
69        .await?)
70}
71
72pub fn frontend_address(idx: u16) -> Option<compact_str::CompactString> {
73    tundra_common::state::frontend_ip(idx).map(|ip| compact_str::format_compact!("{ip}"))
74}
75
76#[inline]
77pub fn alias_of(uuid_short: i32) -> compact_str::CompactString {
78    compact_str::format_compact!("{uuid_short:08x}")
79}
80
81#[inline]
82pub fn is_alias_shaped(name: &str) -> bool {
83    name.len() == 8
84        && name
85            .bytes()
86            .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
87}
88
89pub struct TunnelSigner {
90    pub issuer: JwtIssuer,
91    pub public: Hash32,
92}
93
94static SIGNER: OnceLock<Arc<TunnelSigner>> = OnceLock::new();
95
96pub async fn signer(
97    database: &Database,
98) -> Result<Arc<TunnelSigner>, crate::database::DatabaseError> {
99    if let Some(signer) = SIGNER.get() {
100        return Ok(Arc::clone(signer));
101    }
102
103    let stored: Option<String> = sqlx::query_scalar("SELECT value FROM settings WHERE key = $1")
104        .bind(JWT_KEY_SETTING)
105        .fetch_optional(database.read())
106        .await?;
107
108    let stored = match stored {
109        Some(stored) => stored,
110        None => {
111            let candidate = database
112                .encrypt_base64(hex::encode(rand::random::<[u8; 32]>()))
113                .await?;
114
115            sqlx::query_scalar(
116                r#"
117                INSERT INTO settings (key, value)
118                VALUES ($1, $2)
119                ON CONFLICT (key) DO UPDATE SET value = settings.value
120                RETURNING value
121                "#,
122            )
123            .bind(JWT_KEY_SETTING)
124            .bind(candidate.as_str())
125            .fetch_one(database.write())
126            .await?
127        }
128    };
129
130    let mut seed = [0; 32];
131    hex::decode_to_slice(database.decrypt_base64(stored).await?.as_str(), &mut seed)
132        .map_err(|err| anyhow::anyhow!("the stored tunnel signing key is malformed: {err}"))?;
133
134    let key = ed25519_dalek::SigningKey::from_bytes(&seed);
135    let public = Hash32(key.verifying_key().to_bytes());
136    let issuer = JwtIssuer::from_pkcs8_der(
137        key.to_pkcs8_der()
138            .map_err(|err| anyhow::anyhow!("failed to encode the tunnel signing key: {err}"))?
139            .as_bytes(),
140    );
141
142    Ok(Arc::clone(
143        SIGNER.get_or_init(|| Arc::new(TunnelSigner { issuer, public })),
144    ))
145}
146
147fn proto_of(protocols: &[TunnelProtocol]) -> Proto {
148    match (
149        protocols.contains(&TunnelProtocol::Tcp),
150        protocols.contains(&TunnelProtocol::Udp),
151    ) {
152        (true, true) => Proto::Both,
153        (false, true) => Proto::Udp,
154        _ => Proto::Tcp,
155    }
156}
157
158#[derive(ToSchema, Serialize, Deserialize, Type, PartialEq, Eq, Hash, Clone, Copy)]
159#[serde(rename_all = "snake_case")]
160#[sqlx(type_name = "tunnel_protocol", rename_all = "SCREAMING_SNAKE_CASE")]
161pub enum TunnelProtocol {
162    Tcp,
163    Udp,
164}
165
166pub async fn snapshot(database: &Database) -> Result<Snapshot, crate::database::DatabaseError> {
167    let jwt_pubkey = signer(database).await?.public;
168
169    let mut transaction = database.write().begin().await?;
170    sqlx::query("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ")
171        .execute(&mut *transaction)
172        .await?;
173
174    let epoch = epoch(&mut *transaction).await?;
175
176    let nodes = sqlx::query(
177        r#"
178        SELECT nodes.uuid, nodes.name, node_tunnels.host, node_tunnels.port, node_tunnels.cert_sha256
179        FROM node_tunnels
180        JOIN nodes ON nodes.uuid = node_tunnels.node_uuid
181        "#,
182    )
183    .fetch_all(&mut *transaction)
184    .await?
185    .into_iter()
186    .map(|row| {
187        Ok::<_, crate::database::DatabaseError>(NodeEntry {
188            uuid: row.try_get("uuid")?,
189            name: row.try_get::<String, _>("name")?,
190            host: row.try_get::<String, _>("host")?,
191            tunnel_port: row.try_get::<i32, _>("port")? as u16,
192            cert_sha256: row
193                .try_get::<Option<Vec<u8>>, _>("cert_sha256")?
194                .and_then(|bytes| <[u8; 32]>::try_from(bytes.as_slice()).ok())
195                .map(Hash32),
196        })
197    })
198    .try_collect_vec()?;
199
200    let mut ports: std::collections::HashMap<uuid::Uuid, Vec<PortSpec>> =
201        std::collections::HashMap::new();
202    for row in sqlx::query(
203        r#"
204        SELECT server_tunnel_ports.server_uuid, server_tunnel_ports.port, server_tunnel_ports.protocols
205        FROM server_tunnel_ports
206        ORDER BY server_tunnel_ports.port
207        "#,
208    )
209    .fetch_all(&mut *transaction)
210    .await?
211    {
212        ports
213            .entry(row.try_get("server_uuid")?)
214            .or_default()
215            .push(PortSpec {
216                port: row.try_get::<i32, _>("port")? as u16,
217                proto: proto_of(&row.try_get::<Vec<TunnelProtocol>, _>("protocols")?),
218            });
219    }
220
221    let servers = sqlx::query(
222        r#"
223        SELECT
224            server_tunnels.server_uuid,
225            server_tunnels.idx,
226            server_tunnels.name,
227            servers.node_uuid,
228            servers.uuid_short
229        FROM server_tunnels
230        JOIN servers ON servers.uuid = server_tunnels.server_uuid
231        JOIN node_tunnels ON node_tunnels.node_uuid = servers.node_uuid
232        "#,
233    )
234    .fetch_all(&mut *transaction)
235    .await?
236    .into_iter()
237    .map(|row| {
238        let uuid: uuid::Uuid = row.try_get("server_uuid")?;
239
240        Ok::<_, crate::database::DatabaseError>(ServerEntry {
241            uuid,
242            idx: row.try_get::<i32, _>("idx")? as u16,
243            node_uuid: row.try_get("node_uuid")?,
244            name: row.try_get::<String, _>("name")?,
245            aliases: vec![alias_of(row.try_get::<i32, _>("uuid_short")?).into()],
246            container_ref: String::new(),
247            dial_addr: None,
248            ports: ports.remove(&uuid).unwrap_or_default(),
249        })
250    })
251    .try_collect_vec()?;
252
253    let acls = sqlx::query(
254        r#"
255        SELECT server_tunnel_connections.src_server_uuid, server_tunnel_connections.dst_server_uuid
256        FROM server_tunnel_connections
257        JOIN servers AS src ON src.uuid = server_tunnel_connections.src_server_uuid
258        JOIN node_tunnels AS src_node ON src_node.node_uuid = src.node_uuid
259        JOIN servers AS dst ON dst.uuid = server_tunnel_connections.dst_server_uuid
260        JOIN node_tunnels AS dst_node ON dst_node.node_uuid = dst.node_uuid
261        ORDER BY server_tunnel_connections.src_server_uuid, server_tunnel_connections.dst_server_uuid
262        "#,
263    )
264    .fetch_all(&mut *transaction)
265    .await?
266    .into_iter()
267    .map(|row| {
268        Ok::<_, crate::database::DatabaseError>(AclEntry {
269            src_server: row.try_get("src_server_uuid")?,
270            dst_server: row.try_get("dst_server_uuid")?,
271        })
272    })
273    .try_collect_vec()?;
274
275    transaction.commit().await?;
276
277    Ok(Snapshot {
278        epoch: epoch as u64,
279        jwt_pubkey,
280        nodes,
281        servers,
282        acls,
283    })
284}
285
286const POKE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
287
288/// Tells every node on the mesh to pull the snapshot now rather than on its next poll. Must be
289/// called *after* the commit that bumped the epoch, or a node can win the race and re-read the
290/// state it already has. Batched under one key, so several bumps in a request collapse to one
291/// round of pokes, and spawned so the batch loop is not held for the length of the requests.
292pub async fn poke_nodes(database: &Arc<Database>) {
293    database
294        .batch_action("poke_tunnel_nodes", uuid::Uuid::nil(), {
295            let database = Arc::clone(database);
296
297            async move {
298                tokio::spawn(async move {
299                    if let Err(err) = poke_nodes_now(&database).await {
300                        tracing::warn!("failed to poke the tunnel nodes: {:?}", err);
301                    }
302                });
303
304                Ok(())
305            }
306        })
307        .await;
308}
309
310async fn poke_nodes_now(database: &Database) -> Result<(), crate::database::DatabaseError> {
311    let rows = sqlx::query(
312        r#"
313        SELECT nodes.uuid, nodes.url, nodes.token
314        FROM node_tunnels
315        JOIN nodes ON nodes.uuid = node_tunnels.node_uuid
316        "#,
317    )
318    .fetch_all(database.read())
319    .await?;
320
321    let mut pokes = Vec::with_capacity(rows.len());
322    for row in rows {
323        let uuid: uuid::Uuid = row.try_get("uuid")?;
324        let client = wings_api::client::WingsClient::new(
325            row.try_get::<String, _>("url")?,
326            database
327                .decrypt(row.try_get::<Vec<u8>, _>("token")?)
328                .await?
329                .into(),
330        );
331
332        pokes.push(tokio::spawn(async move {
333            match tokio::time::timeout(POKE_TIMEOUT, client.post_tundra_sync()).await {
334                Ok(Ok(())) => {}
335                Ok(Err(err)) => tracing::debug!(node = %uuid, "failed to poke node: {:?}", err),
336                Err(_) => tracing::debug!(node = %uuid, "poking node timed out"),
337            }
338        }));
339    }
340
341    for poke in pokes {
342        let _ = poke.await;
343    }
344
345    Ok(())
346}