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
17pub 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
288pub 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}