Compare commits

...

19 Commits

Author SHA1 Message Date
e565752562 Fix workspace paths for cargo-rpm packaging
Some checks failed
ci / Rust Checks (push) Failing after 6m26s
release / release (push) Failing after 5m15s
2026-02-24 12:47:22 +00:00
d23bf52c50 Improve CI checks and fix RPM release step
Some checks failed
ci / Rust Checks (push) Failing after 6m49s
release / release (push) Failing after 5m19s
2026-02-24 12:12:11 +00:00
ea5a0861fd Fix release workflow YAML parsing
Some checks failed
ci / build (push) Successful in 2m23s
release / release (push) Failing after 5m16s
2026-02-24 11:19:53 +00:00
cb7257ed6f Update .gitea/workflows/release.yml
All checks were successful
ci / build (push) Successful in 2m17s
2026-02-24 10:33:21 +00:00
6c7730eb7f Fix release workflow python heredoc
Some checks failed
ci / build (push) Has been cancelled
2026-02-24 10:31:03 +00:00
31fc9fe654 Fix release workflow heredoc formatting
Some checks failed
ci / build (push) Has been cancelled
2026-02-24 10:29:54 +00:00
4b22a19837 Use TOKEN/USERNAME secrets for release workflow
Some checks failed
ci / build (push) Has been cancelled
2026-02-24 10:28:46 +00:00
544cbaf553 Split release workflow and fix Gitea compatibility
Some checks failed
ci / build (push) Has been cancelled
2026-02-24 10:27:33 +00:00
1893e63d08 Add release packaging and metadata 2026-02-24 10:25:11 +00:00
d2db097445 Polish metrics and validate config
All checks were successful
ci / build (push) Successful in 2m27s
2026-02-24 10:12:54 +00:00
f0c5261e7b Add Gitea CI build workflow
All checks were successful
ci / build (push) Successful in 3m12s
2026-02-24 10:05:25 +00:00
2f75b75703 Add r2r prioritization, QUIC transport, and redis limits
# Conflicts:
#	Cargo.toml
#	relay/src/main.rs
2026-02-24 09:47:37 +00:00
356b049849 Delete README.md 2026-02-24 09:45:01 +00:00
L
230a9212fe refactor: remove stripe webhook flow for free product 2026-02-24 08:26:38 +00:00
L
28918880da feat: add hybrid local and redis rate limiting enforcement 2026-02-24 08:25:28 +00:00
L
37090d80b0 feat: pool and multiplex relay-to-relay tcp channels 2026-02-24 08:24:50 +00:00
L
a45a9b0392 feat: add postgres auth sync and stripe webhook verification 2026-02-23 23:32:48 +00:00
L
4ce94a5b17 feat: add prometheus metrics and tracing instrumentation 2026-02-23 23:30:07 +00:00
L
fe8376dd6d feat: add relay rate limiting and bandwidth token buckets 2026-02-23 23:27:13 +00:00
13 changed files with 2527 additions and 150 deletions

37
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,37 @@
name: ci
on:
push:
pull_request:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
checks:
name: Rust Checks
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Cache Rust build
uses: Swatinem/rust-cache@v2
- name: Cargo check
run: cargo check --workspace --all-targets --locked
- name: Cargo fmt
run: cargo fmt --all -- --check
- name: Cargo clippy
run: cargo clippy --workspace --all-targets --locked -- -D warnings
- name: Cargo test
run: cargo test --workspace --all-targets --locked

View File

@@ -0,0 +1,201 @@
name: release
on:
push:
branches:
- main
tags:
- 'v*'
permissions:
contents: write
packages: write
jobs:
release:
runs-on: ubuntu-latest
env:
OWNER: ${{ github.repository_owner }}
REPO: ${{ github.event.repository.name }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install packaging tools
run: |
cargo install cargo-deb --locked
cargo install cargo-rpm --locked
- name: Build release binaries
run: cargo build --release -p relay -p client
- name: Resolve version
id: version
run: |
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF#refs/tags/}"
IS_PRERELEASE=false
else
VERSION="main-${GITHUB_SHA:0:7}"
IS_PRERELEASE=true
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "is_prerelease=${IS_PRERELEASE}" >> "$GITHUB_OUTPUT"
- name: Package tarball
run: |
mkdir -p dist
cp target/release/relay dist/
cp target/release/client dist/
tar -czf "dist/play-dvv-${{ steps.version.outputs.version }}-linux-x86_64.tar.gz" -C dist relay client
- name: Build deb packages
run: |
cargo deb -p relay --no-build
cargo deb -p client --no-build
cp target/debian/*.deb dist/ || true
- name: Build rpm packages
run: |
mkdir -p relay/target/release client/target/release
cp target/release/relay relay/target/release/relay
cp target/release/client client/target/release/client
(cd relay && cargo rpm build --release)
(cd client && cargo rpm build --release)
find . -path "*/target/release/rpmbuild/RPMS/*.rpm" -exec cp {} dist/ \;
# -------------------------------------------------
# FIXED: Generic package upload (Gitea correct API)
# -------------------------------------------------
- name: Publish generic packages
env:
TOKEN: ${{ secrets.TOKEN }}
run: |
if [[ -z "${TOKEN}" ]]; then
echo "TOKEN missing; skipping generic packages"
exit 0
fi
VERSION="${{ steps.version.outputs.version }}"
for file in dist/*; do
name=$(basename "$file")
curl -sSf -X PUT \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"$file" \
"${GITHUB_SERVER_URL}/api/packages/${OWNER}/generic/${REPO}/${VERSION}/${name}"
done
# -------------------------------------------------
# OCI Images (fixed repo casing issue)
# -------------------------------------------------
- name: Build and push OCI images
env:
TOKEN: ${{ secrets.TOKEN }}
USERNAME: ${{ secrets.USERNAME }}
run: |
if [[ -z "${TOKEN}" || -z "${USERNAME}" ]]; then
echo "TOKEN or USERNAME missing; skipping OCI"
exit 0
fi
VERSION="${{ steps.version.outputs.version }}"
IMAGE_BASE="git.dvv.one/${OWNER}/${REPO}"
echo "${TOKEN}" | docker login git.dvv.one -u "${USERNAME}" --password-stdin
printf '%s\n' \
'FROM debian:bookworm-slim' \
'COPY target/release/relay /usr/local/bin/relay' \
'EXPOSE 7000 7001 25565' \
'ENTRYPOINT ["/usr/local/bin/relay"]' \
> Dockerfile.relay
printf '%s\n' \
'FROM debian:bookworm-slim' \
'COPY target/release/client /usr/local/bin/client' \
'ENTRYPOINT ["/usr/local/bin/client"]' \
> Dockerfile.client
docker build -f Dockerfile.relay -t ${IMAGE_BASE}/relay:${VERSION} .
docker build -f Dockerfile.client -t ${IMAGE_BASE}/client:${VERSION} .
if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then
docker tag ${IMAGE_BASE}/relay:${VERSION} ${IMAGE_BASE}/relay:main
docker tag ${IMAGE_BASE}/client:${VERSION} ${IMAGE_BASE}/client:main
fi
docker push ${IMAGE_BASE}/relay:${VERSION}
docker push ${IMAGE_BASE}/client:${VERSION}
if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then
docker push ${IMAGE_BASE}/relay:main
docker push ${IMAGE_BASE}/client:main
fi
# -------------------------------------------------
# FIXED: Proper Gitea Cargo registry setup
# -------------------------------------------------
- name: Configure Cargo for Gitea
if: startsWith(github.ref, 'refs/tags/')
run: |
mkdir -p ~/.cargo
printf '%s\n' \
'[registries.gitea]' \
"index = \"sparse+${GITHUB_SERVER_URL}/api/packages/${OWNER}/cargo/\"" \
> ~/.cargo/config.toml
- name: Publish Cargo packages
if: startsWith(github.ref, 'refs/tags/')
env:
CARGO_REGISTRIES_GITEA_TOKEN: ${{ secrets.TOKEN }}
run: |
TAG="${GITHUB_REF#refs/tags/}"
RELAY_VER=$(cargo metadata --format-version 1 | jq -r '.packages[] | select(.name=="relay") | .version')
CLIENT_VER=$(cargo metadata --format-version 1 | jq -r '.packages[] | select(.name=="client") | .version')
if [[ "v${RELAY_VER}" == "${TAG}" ]]; then
cargo publish -p relay --registry gitea
fi
if [[ "v${CLIENT_VER}" == "${TAG}" ]]; then
cargo publish -p client --registry gitea
fi
# -------------------------------------------------
# FIXED: Gitea Release Creation
# -------------------------------------------------
- name: Create release
env:
TOKEN: ${{ secrets.TOKEN }}
run: |
if [[ -z "${TOKEN}" ]]; then
echo "TOKEN missing; skipping release"
exit 0
fi
VERSION="${{ steps.version.outputs.version }}"
PRERELEASE=${{ steps.version.outputs.is_prerelease }}
release=$(curl -sSf -X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"${VERSION}\",\"target_commitish\":\"${GITHUB_SHA}\",\"name\":\"${VERSION}\",\"body\":\"Automated release for ${VERSION}.\",\"prerelease\":${PRERELEASE}}" \
"${GITHUB_SERVER_URL}/api/v1/repos/${OWNER}/${REPO}/releases")
release_id=$(echo "$release" | jq -r '.id')
for file in dist/*; do
name=$(basename "$file")
curl -sSf -X POST \
-H "Authorization: token ${TOKEN}" \
-F "attachment=@${file}" \
"${GITHUB_SERVER_URL}/api/v1/repos/${OWNER}/${REPO}/releases/${release_id}/assets?name=${name}"
done

8
.yamllint Normal file
View File

@@ -0,0 +1,8 @@
extends: default
rules:
document-start: disable
line-length: disable
truthy:
allowed-values: ['true', 'false', 'on', 'off']
check-keys: true

1229
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,3 +19,13 @@ axum = "0.8"
redis = { version = "0.32", features = ["tokio-comp", "connection-manager"] } redis = { version = "0.32", features = ["tokio-comp", "connection-manager"] }
jsonwebtoken = "10" jsonwebtoken = "10"
chrono = { version = "0.4", features = ["serde", "clock"] } chrono = { version = "0.4", features = ["serde", "clock"] }
metrics = "0.24"
metrics-exporter-prometheus = "0.17"
tokio-postgres = { version = "0.7", features = ["with-chrono-0_4", "with-serde_json-1"] }
hmac = "0.12"
sha2 = "0.10"
hex = "0.4"
quinn = "0.11"
rustls = "0.23"
rcgen = "0.12"
rustls-pemfile = "2.1"

View File

View File

@@ -15,3 +15,7 @@ tokio.workspace = true
tracing.workspace = true tracing.workspace = true
tracing-subscriber.workspace = true tracing-subscriber.workspace = true
fastrand.workspace = true fastrand.workspace = true
metrics.workspace = true
metrics-exporter-prometheus.workspace = true
tokio-postgres.workspace = true
uuid.workspace = true

View File

@@ -1,4 +1,4 @@
use std::{net::SocketAddr, sync::Arc}; use std::{net::SocketAddr, sync::Arc, time::Instant};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use axum::{ use axum::{
@@ -10,14 +10,19 @@ use axum::{
}; };
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode}; use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle};
use redis::AsyncCommands; use redis::AsyncCommands;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio_postgres::{Client as PgClient, NoTls};
use tracing::{info, warn}; use tracing::{info, warn};
use uuid::Uuid;
#[derive(Clone)] #[derive(Clone)]
struct AppState { struct AppState {
jwt_secret: Arc<String>, jwt_secret: Arc<String>,
redis: Option<redis::aio::ConnectionManager>, redis: Option<redis::aio::ConnectionManager>,
pg: Option<Arc<PgClient>>,
metrics: PrometheusHandle,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
@@ -58,12 +63,9 @@ struct ValidateResponse {
max_tunnels: Option<u32>, max_tunnels: Option<u32>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Clone)]
struct StripeWebhookEvent { struct PlanEntitlement {
event_type: String,
user_id: String,
tier: String, tier: String,
#[serde(default = "default_max_tunnels")]
max_tunnels: u32, max_tunnels: u32,
} }
@@ -78,6 +80,10 @@ async fn main() -> Result<()> {
let bind = std::env::var("AUTH_BIND").unwrap_or_else(|_| "0.0.0.0:8080".into()); let bind = std::env::var("AUTH_BIND").unwrap_or_else(|_| "0.0.0.0:8080".into());
let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "dev-secret-change-me".into()); let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "dev-secret-change-me".into());
let metrics = PrometheusBuilder::new()
.install_recorder()
.context("install prometheus recorder")?;
let redis = if let Ok(url) = std::env::var("REDIS_URL") { let redis = if let Ok(url) = std::env::var("REDIS_URL") {
let client = redis::Client::open(url.clone()).context("open redis client")?; let client = redis::Client::open(url.clone()).context("open redis client")?;
match redis::aio::ConnectionManager::new(client).await { match redis::aio::ConnectionManager::new(client).await {
@@ -94,16 +100,20 @@ async fn main() -> Result<()> {
None None
}; };
let pg = connect_postgres().await?;
let state = AppState { let state = AppState {
jwt_secret: Arc::new(jwt_secret), jwt_secret: Arc::new(jwt_secret),
redis, redis,
pg,
metrics,
}; };
let app = Router::new() let app = Router::new()
.route("/healthz", get(healthz)) .route("/healthz", get(healthz))
.route("/metrics", get(metrics_endpoint))
.route("/v1/token/dev", post(issue_dev_token)) .route("/v1/token/dev", post(issue_dev_token))
.route("/v1/token/validate", post(validate_token)) .route("/v1/token/validate", post(validate_token))
.route("/v1/stripe/webhook", post(stripe_webhook))
.with_state(state); .with_state(state);
let listener = tokio::net::TcpListener::bind(&bind) let listener = tokio::net::TcpListener::bind(&bind)
@@ -115,14 +125,36 @@ async fn main() -> Result<()> {
Ok(()) Ok(())
} }
async fn connect_postgres() -> Result<Option<Arc<PgClient>>> {
let Some(url) = std::env::var("DATABASE_URL").ok() else {
return Ok(None);
};
let (client, conn) = tokio_postgres::connect(&url, NoTls)
.await
.context("connect postgres")?;
tokio::spawn(async move {
if let Err(e) = conn.await {
warn!(error = %e, "postgres connection task ended");
}
});
info!("auth-api connected to postgres");
Ok(Some(Arc::new(client)))
}
async fn healthz() -> &'static str { async fn healthz() -> &'static str {
"ok" "ok"
} }
async fn metrics_endpoint(State(state): State<AppState>) -> impl IntoResponse {
state.metrics.render()
}
#[tracing::instrument(skip(state))]
async fn issue_dev_token( async fn issue_dev_token(
State(state): State<AppState>, State(state): State<AppState>,
Json(req): Json<DevTokenRequest>, Json(req): Json<DevTokenRequest>,
) -> Result<Json<TokenResponse>, ApiError> { ) -> Result<Json<TokenResponse>, ApiError> {
let started = Instant::now();
let now = Utc::now(); let now = Utc::now();
let exp = now + Duration::hours(24); let exp = now + Duration::hours(24);
let claims = Claims { let claims = Claims {
@@ -141,9 +173,68 @@ async fn issue_dev_token(
) )
.map_err(ApiError::internal)?; .map_err(ApiError::internal)?;
sync_jwt_cache(&state, &claims).await?;
metrics::counter!("auth_jwt_issued_total").increment(1);
metrics::histogram!("auth_issue_token_latency_ms")
.record(started.elapsed().as_secs_f64() * 1000.0);
Ok(Json(TokenResponse {
token,
expires_at: exp.timestamp(),
}))
}
#[tracing::instrument(skip(state, req))]
async fn validate_token(
State(state): State<AppState>,
Json(req): Json<ValidateRequest>,
) -> Result<Json<ValidateResponse>, ApiError> {
let started = Instant::now();
let decoded = decode::<Claims>(
&req.token,
&DecodingKey::from_secret(state.jwt_secret.as_bytes()),
&Validation::new(Algorithm::HS256),
);
let response = match decoded {
Ok(tok) => {
let claims = tok.claims;
let ent = match load_plan_for_user(&state, &claims.sub).await? {
Some(db_plan) => db_plan,
None => PlanEntitlement {
tier: claims.tier.clone(),
max_tunnels: claims.max_tunnels,
},
};
sync_plan_cache(&state, &claims.sub, &ent, "auth-validate").await?;
metrics::counter!("auth_token_validate_total", "result" => "valid").increment(1);
ValidateResponse {
valid: true,
user_id: Some(claims.sub),
tier: Some(ent.tier),
max_tunnels: Some(ent.max_tunnels),
}
}
Err(_) => {
metrics::counter!("auth_token_validate_total", "result" => "invalid").increment(1);
ValidateResponse {
valid: false,
user_id: None,
tier: None,
max_tunnels: None,
}
}
};
metrics::histogram!("auth_validate_token_latency_ms")
.record(started.elapsed().as_secs_f64() * 1000.0);
Ok(Json(response))
}
async fn sync_jwt_cache(state: &AppState, claims: &Claims) -> Result<(), ApiError> {
if let Some(mut redis) = state.redis.clone() { if let Some(mut redis) = state.redis.clone() {
let key = format!("auth:jwt:jti:{}", claims.jti); let key = format!("auth:jwt:jti:{}", claims.jti);
let ttl = (claims.exp as i64 - now.timestamp()).max(1); let ttl = (claims.exp as i64 - Utc::now().timestamp()).max(1);
let payload = serde_json::json!({ let payload = serde_json::json!({
"user_id": claims.sub, "user_id": claims.sub,
"plan_tier": claims.tier, "plan_tier": claims.tier,
@@ -155,69 +246,53 @@ async fn issue_dev_token(
.await .await
.map_err(ApiError::internal)?; .map_err(ApiError::internal)?;
} }
Ok(())
Ok(Json(TokenResponse {
token,
expires_at: exp.timestamp(),
}))
} }
async fn validate_token( async fn sync_plan_cache(
State(state): State<AppState>, state: &AppState,
Json(req): Json<ValidateRequest>, user_id: &str,
) -> Result<Json<ValidateResponse>, ApiError> { ent: &PlanEntitlement,
let decoded = decode::<Claims>( source: &str,
&req.token, ) -> Result<(), ApiError> {
&DecodingKey::from_secret(state.jwt_secret.as_bytes()),
&Validation::new(Algorithm::HS256),
);
match decoded {
Ok(tok) => {
let c = tok.claims;
if let Some(mut redis) = state.redis.clone() { if let Some(mut redis) = state.redis.clone() {
let key = format!("plan:user:{}", c.sub); let key = format!("plan:user:{user_id}");
let payload = serde_json::json!({ let payload = serde_json::json!({
"tier": c.tier, "tier": ent.tier,
"max_tunnels": c.max_tunnels, "max_tunnels": ent.max_tunnels,
"source": "auth-api" "source": source,
"updated_at": Utc::now().timestamp()
}) })
.to_string(); .to_string();
let _: () = redis.set_ex(key, payload, 300).await.map_err(ApiError::internal)?; let _: () = redis.set_ex(key, payload, 300).await.map_err(ApiError::internal)?;
} }
Ok(Json(ValidateResponse { Ok(())
valid: true,
user_id: Some(c.sub),
tier: Some(c.tier),
max_tunnels: Some(c.max_tunnels),
}))
}
Err(_) => Ok(Json(ValidateResponse {
valid: false,
user_id: None,
tier: None,
max_tunnels: None,
})),
}
} }
async fn stripe_webhook( async fn load_plan_for_user(state: &AppState, user_id: &str) -> Result<Option<PlanEntitlement>, ApiError> {
State(state): State<AppState>, let Some(pg) = &state.pg else {
Json(event): Json<StripeWebhookEvent>, return Ok(None);
) -> Result<impl IntoResponse, ApiError> { };
if let Some(mut redis) = state.redis.clone() { let _ = Uuid::parse_str(user_id).map_err(ApiError::bad_request)?;
let key = format!("plan:user:{}", event.user_id); let row = pg
let payload = serde_json::json!({ .query_opt(
"tier": event.tier, r#"
"max_tunnels": event.max_tunnels, select p.id as plan_id, p.max_tunnels
"source": "stripe_webhook", from subscriptions s
"last_event_type": event.event_type, join plans p on p.id = s.plan_id
"updated_at": Utc::now().timestamp(), where s.user_id = $1::uuid and s.status in ('active', 'trialing')
}) order by s.updated_at desc
.to_string(); limit 1
let _: () = redis.set_ex(key, payload, 300).await.map_err(ApiError::internal)?; "#,
} &[&user_id],
Ok(StatusCode::NO_CONTENT) )
.await
.map_err(ApiError::internal)?;
Ok(row.map(|r| PlanEntitlement {
tier: r.get::<_, String>("plan_id"),
max_tunnels: r.get::<_, i32>("max_tunnels") as u32,
}))
} }
#[derive(Debug)] #[derive(Debug)]
@@ -233,6 +308,14 @@ impl ApiError {
message: e.to_string(), message: e.to_string(),
} }
} }
fn bad_request<E: std::fmt::Display>(e: E) -> Self {
Self {
status: StatusCode::BAD_REQUEST,
message: e.to_string(),
}
}
} }
impl IntoResponse for ApiError { impl IntoResponse for ApiError {

View File

@@ -2,6 +2,23 @@
name = "client" name = "client"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
description = "Play-DVV client"
license = "MIT"
authors = ["Play-DVV Team <ops@dvv.one>"]
[package.metadata.deb]
maintainer = "Play-DVV Team <ops@dvv.one>"
section = "net"
priority = "optional"
assets = [
["target/release/client", "usr/bin/client", "755"]
]
[package.metadata.rpm]
package = "client"
assets = [
{ source = "target/release/client", dest = "/usr/bin/client", mode = "755" }
]
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true

View File

@@ -57,6 +57,30 @@ pub struct RelayForwardPrelude {
pub initial_data: Vec<u8>, pub initial_data: Vec<u8>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct R2rStreamData {
pub session_id: String,
pub stream_id: String,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct R2rStreamClosed {
pub session_id: String,
pub stream_id: String,
pub reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
pub enum R2rFrame {
Open(RelayForwardPrelude),
Data(R2rStreamData),
Close(R2rStreamClosed),
Ping,
Pong,
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "data")] #[serde(tag = "type", content = "data")]
pub enum ClientFrame { pub enum ClientFrame {

View File

@@ -35,6 +35,9 @@ create table if not exists subscriptions (
); );
create index if not exists subscriptions_user_id_idx on subscriptions(user_id); create index if not exists subscriptions_user_id_idx on subscriptions(user_id);
create unique index if not exists subscriptions_provider_subscription_id_uidx
on subscriptions(provider_subscription_id)
where provider_subscription_id is not null;
create table if not exists tunnels ( create table if not exists tunnels (
id uuid primary key default gen_random_uuid(), id uuid primary key default gen_random_uuid(),

View File

@@ -2,6 +2,23 @@
name = "relay" name = "relay"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
description = "Play-DVV relay server"
license = "MIT"
authors = ["Play-DVV Team <ops@dvv.one>"]
[package.metadata.deb]
maintainer = "Play-DVV Team <ops@dvv.one>"
section = "net"
priority = "optional"
assets = [
["target/release/relay", "usr/bin/relay", "755"]
]
[package.metadata.rpm]
package = "relay"
assets = [
{ source = "target/release/relay", dest = "/usr/bin/relay", mode = "755" }
]
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
@@ -14,4 +31,10 @@ redis.workspace = true
serde_json.workspace = true serde_json.workspace = true
chrono.workspace = true chrono.workspace = true
serde.workspace = true serde.workspace = true
metrics.workspace = true
metrics-exporter-prometheus.workspace = true
common = { path = "../common" } common = { path = "../common" }
quinn.workspace = true
rustls.workspace = true
rcgen.workspace = true
rustls-pemfile.workspace = true

File diff suppressed because it is too large Load Diff