Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Sichere ProgrammierungI audited my own ML linter and had to withdraw its best evidence(21.09.2026 um 22:54 Uhr)
Sichere ProgrammierungQuantum Result Validation for Distributed Computing Systems(21.09.2026 um 22:54 Uhr)
Sichere ProgrammierungJWT Authentication and Role-Based Access Control in LocalHands(21.09.2026 um 22:56 Uhr)
Sichere ProgrammierungStochastic Parrot or Alien Mind?(21.09.2026 um 22:56 Uhr)
Sichere ProgrammierungBuilding AI for the Physical World Is a Different Engineering Problem(21.09.2026 um 22:58 Uhr)
Sichere ProgrammierungI audited my own ML linter and had to withdraw its best evidence(21.09.2026 um 22:54 Uhr)
Sichere ProgrammierungQuantum Result Validation for Distributed Computing Systems(21.09.2026 um 22:54 Uhr)
Sichere ProgrammierungJWT Authentication and Role-Based Access Control in LocalHands(21.09.2026 um 22:56 Uhr)
Sichere ProgrammierungStochastic Parrot or Alien Mind?(21.09.2026 um 22:56 Uhr)
Sichere ProgrammierungBuilding AI for the Physical World Is a Different Engineering Problem(21.09.2026 um 22:58 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Building Streaky: Solving Cloudflare IP Blocking with Rust (Part 2)

Part 2: The Notification Proxy In Part 1, I shared how I built a distributed cron system to process users in parallel. But there was another critical problem I had to solve: Cloudflare Workers' shared IP pool was being blocked by Discord…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!




Part 2: The Notification Proxy



In Part 1, I shared how I built a distributed cron system to process users in parallel. But there was another critical problem I had to solve: Cloudflare Workers' shared IP pool was being blocked by Discord and Telegram APIs.



This is the story of how I built a Rust notification proxy to solve that problem while maintaining end-to-end encryption.









The IP Blocking Problem



When I first tried sending Discord notifications directly from Cloudflare Workers, I got this:




{
"message": "You are being rate limited.",
"retry_after": 64.0,
"global": false,
"code": 429
}






Every. Single. Time.



But when I tested the same webhook URL with curl from my laptop, it worked perfectly. What was going on?






The Root Cause



Cloudflare Workers use a shared IP pool. Thousands of Workers across different accounts make requests from the same set of IP addresses. Discord and Telegram's APIs see this pattern and flag it as bot/spam traffic.



From their perspective:




  • Same IP making thousands of requests per hour

  • Different webhooks/tokens

  • Looks like a bot farm



Result: Aggressive rate limiting or outright blocking.






Why This Matters



This isn't just a Streaky problem. Any Cloudflare Worker trying to call Discord, Telegram, or similar APIs will hit this issue. It's a fundamental constraint of the shared infrastructure.



Possible solutions:




  1. Use Cloudflare's paid tier with dedicated IPs (expensive)

  2. Proxy through a service with clean IPs (adds complexity)

  3. Build your own proxy (what I chose)









The Solution: Rust VPS Proxy



I decided to build a lightweight Rust proxy on Koyeb's free tier. The architecture:




Cloudflare Worker (Shared IP, blocked)
|
| HTTPS + Auth Header
| Data: ENCRYPTED
v
Rust VPS on Koyeb (Clean IP, not blocked)
|
| Decrypt credentials
| Forward to Discord/Telegram
v
Discord/Telegram APIs (Success!)









Why Rust?



The real reason: Performance and resource efficiency on free tier.



Koyeb's free tier gives you 512MB RAM and 0.1 vCPU. With those constraints, I needed something that could:




  • Handle hundreds of concurrent requests

  • Use minimal memory (< 50MB)

  • Fast response times (< 100ms processing)

  • Small binary size (< 10MB)



Rust delivers:




  • Blazing fast (comparable to C/C++)

  • Tiny memory footprint (~20MB idle)

  • Small binary size (5MB vs Node.js 50MB+)

  • Zero-cost abstractions

  • No garbage collection pauses



The math:




  • Node.js: 50MB base + 10MB per 100 concurrent requests = 150MB+ for moderate load

  • Rust: 20MB base + 2MB per 100 concurrent requests = 40MB for same load



On a 512MB VPS, Rust can handle 10x more traffic than Node.js. That's the difference between staying on free tier vs paying for resources.









Building the Rust Proxy






Tech Stack





  • Framework: Axum (lightweight, fast, ergonomic)


  • Async Runtime: Tokio


  • HTTP Client: Reqwest


  • Encryption: AES-GCM (aes-gcm crate)


  • Serialization: Serde


  • Deployment: Docker on Koyeb






Project Structure






server/
├── src/
│ ├── main.rs # Axum server setup
│ ├── handlers.rs # API endpoints
│ ├── encryption.rs # AES-256-GCM decryption
│ ├── discord.rs # Discord webhook sender
│ └── telegram.rs # Telegram bot API sender
├── Cargo.toml # Dependencies
└── Dockerfile # Container config









Dependencies (Cargo.toml)






[package]
name = "streaky-notification-proxy"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
aes-gcm = "0.10"
base64 = "0.21"
tower-http = { version = "0.5", features = ["cors"] }












Implementation






1. Main Server (main.rs)






use axum::{
routing::{get, post},
Router,
};
use std::net::SocketAddr;
use tower_http::cors::CorsLayer;

mod handlers;
mod encryption;
mod discord;
mod telegram;

#[tokio::main]
async fn main() {
// Initialize encryption service
let encryption_key = std::env::var("ENCRYPTION_KEY")
.expect("ENCRYPTION_KEY must be set");

// Build router
let app = Router::new()
.route("/", get(health_check))
.route("/send-notification", post(handlers::send_notification))
.layer(CorsLayer::permissive());

// Start server
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
println!("Server listening on {}", addr);

axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}

async fn health_check() -> &'static str {
"OK"
}









2. Encryption Service (encryption.rs)



The critical part: decrypting credentials sent from Workers.




use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Nonce,
};
use base64::{engine::general_purpose, Engine as _};

pub struct EncryptionService {
cipher: Aes256Gcm,
}

impl EncryptionService {
pub fn new(key: &str) -> Result<Self, Box<dyn std::error::Error>> {
// Key must be exactly 32 bytes for AES-256
if key.len() < 32 {
return Err("Encryption key must be at least 32 characters".into());
}

let key_bytes = key.as_bytes();
let key_array: [u8; 32] = key_bytes[..32].try_into()?;
let cipher = Aes256Gcm::new(&key_array.into());

Ok(Self { cipher })
}

pub fn decrypt(&self, encrypted_data: &str) -> Result<String, Box<dyn std::error::Error>> {
// Decode from base64 (try STANDARD first, fallback to URL_SAFE)
let encrypted_bytes = general_purpose::STANDARD
.decode(encrypted_data)
.or_else(|_| general_purpose::URL_SAFE.decode(encrypted_data))?;

// Extract IV (first 12 bytes) and ciphertext (rest)
if encrypted_bytes.len() < 12 {
return Err("Invalid encrypted data: too short".into());
}

let (iv_bytes, ciphertext) = encrypted_bytes.split_at(12);
let nonce: &[u8; 12] = iv_bytes.try_into()?;

// Decrypt
let plaintext = self
.cipher
.decrypt(nonce.into(), ciphertext)
.map_err(|e| format!("Decryption failed: {:?}", e))?;

// Convert to string
Ok(String::from_utf8(plaintext)?)
}
}






Key points:




  • AES-256-GCM for authenticated encryption

  • 12-byte IV (nonce) prepended to ciphertext

  • Base64 decoding with fallback (STANDARD or URL_SAFE)

  • Same algorithm as Worker encryption service






3. Request Handler (handlers.rs)






use axum::{
extract::Json,
http::StatusCode,
};
use serde::{Deserialize, Serialize};

use crate::{discord, telegram, encryption::EncryptionService};

#[derive(Deserialize)]
pub struct NotificationRequest {
#[serde(rename = "type")]
pub notification_type: String,
pub encrypted_webhook: Option<String>,
pub encrypted_token: Option<String>,
pub encrypted_chat_id: Option<String>,
pub message: MessagePayload,
}

#[derive(Deserialize, Serialize)]
pub struct MessagePayload {
pub username: String,
pub current_streak: i32,
pub contributions_today: i32,
pub message: String,
}

#[derive(Serialize)]
pub struct NotificationResponse {
pub success: bool,
pub error: Option<String>,
}

pub async fn send_notification(
Json(payload): Json<NotificationRequest>,
) -> Result<Json<NotificationResponse>, StatusCode> {
// Validate API secret
let vps_secret = std::env::var("VPS_SECRET")
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

// In production, check X-API-Secret header here

// Initialize encryption service
let encryption_key = std::env::var("ENCRYPTION_KEY")
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

let encryption_service = EncryptionService::new(&encryption_key)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

// Route based on notification type
match payload.notification_type.as_str() {
"discord" => {
let webhook = payload.encrypted_webhook
.ok_or(StatusCode::BAD_REQUEST)?;

// Decrypt webhook URL
let decrypted_webhook = encryption_service
.decrypt(&webhook)
.map_err(|_| StatusCode::BAD_REQUEST)?;

// Send to Discord
discord::send_discord_notification(&decrypted_webhook, &payload.message)
.await
.map(|_| Json(NotificationResponse {
success: true,
error: None,
}))
.map_err(|e| {
eprintln!("Discord error: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})
}
"telegram" => {
let token = payload.encrypted_token
.ok_or(StatusCode::BAD_REQUEST)?;
let chat_id = payload.encrypted_chat_id
.ok_or(StatusCode::BAD_REQUEST)?;

// Decrypt credentials
let decrypted_token = encryption_service
.decrypt(&token)
.map_err(|_| StatusCode::BAD_REQUEST)?;
let decrypted_chat_id = encryption_service
.decrypt(&chat_id)
.map_err(|_| StatusCode::BAD_REQUEST)?;

// Send to Telegram
telegram::send_telegram_notification(
&decrypted_token,
&decrypted_chat_id,
&payload.message,
)
.await
.map(|_| Json(NotificationResponse {
success: true,
error: None,
}))
.map_err(|e| {
eprintln!("Telegram error: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})
}
_ => Err(StatusCode::BAD_REQUEST),
}
}









4. Discord Integration (discord.rs)






use reqwest::Client;
use serde_json::json;

use crate::handlers::MessagePayload;

pub async fn send_discord_notification(
webhook_url: &str,
message: &MessagePayload,
) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();

// Build Discord embed
let embed = json!({
"title": format!("GitHub Streak Update - {}", message.username),
"description": message.message,
"color": if message.contributions_today > 0 { 0x00ff00 } else { 0xff0000 },
"fields": [
{
"name": "Current Streak",
"value": format!("{} days", message.current_streak),
"inline": true
},
{
"name": "Contributions Today",
"value": message.contributions_today.to_string(),
"inline": true
}
],
"timestamp": chrono::Utc::now().to_rfc3339()
});

let payload = json!({
"embeds": [embed]
});

// Send to Discord
let response = client
.post(webhook_url)
.json(&payload)
.timeout(std::time::Duration::from_secs(10))
.send()
.await?;

if !response.status().is_success() {
let error_text = response.text().await?;
return Err(format!("Discord API error: {}", error_text).into());
}

Ok(())
}









5. Telegram Integration (telegram.rs)






use reqwest::Client;
use serde_json::json;

use crate::handlers::MessagePayload;

pub async fn send_telegram_notification(
bot_token: &str,
chat_id: &str,
message: &MessagePayload,
) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();

// Build Telegram message (Markdown format)
let text = format!(
"*GitHub Streak Update - {}*\n\n{}\n\n*Current Streak:* {} days\n*Contributions Today:* {}",
message.username,
message.message,
message.current_streak,
message.contributions_today
);

let payload = json!({
"chat_id": chat_id,
"text": text,
"parse_mode": "Markdown"
});

// Send to Telegram
let url = format!("https://api.telegram.org/bot{}/sendMessage", bot_token);

let response = client
.post(&url)
.json(&payload)
.timeout(std::time::Duration::from_secs(10))
.send()
.await?;

if !response.status().is_success() {
let error_text = response.text().await?;
return Err(format!("Telegram API error: {}", error_text).into());
}

Ok(())
}












Deployment






Dockerfile






FROM rust:1.75 as builder

WORKDIR /app

# Copy manifests
COPY Cargo.toml Cargo.lock ./

# Copy source code
COPY src ./src

# Build release binary
RUN cargo build --release

# Runtime stage
FROM debian:bookworm-slim

# Install CA certificates for HTTPS
RUN apt-get update && \
apt-get install -y ca-certificates && \
rm -rf /var/lib/apt/lists/*

# Copy binary from builder
COPY --from=builder /app/target/release/streaky-notification-proxy /usr/local/bin/

# Expose port
EXPOSE 3000

# Run binary
CMD ["streaky-notification-proxy"]









Koyeb Configuration



Environment Variables:




ENCRYPTION_KEY=your-32-character-encryption-key-here
VPS_SECRET=your-vps-secret-uuid-here
PORT=3000






Resources:




  • Memory: 512MB (free tier)

  • CPU: 0.1 vCPU

  • Region: Frankfurt (closest to Cloudflare edge)



Deployment:




  1. Push to GitHub

  2. Connect Koyeb to repo

  3. Set environment variables

  4. Deploy from server/ directory

  5. Get public URL









Worker Integration



Now the Worker can call the Rust proxy instead of Discord/Telegram directly:




// web/backend/src/services/notifications.ts

export class NotificationService {
constructor(private env: Env) {}

async sendDiscordNotification(
encryptedWebhook: string,
message: NotificationMessage
): Promise<NotificationResult> {
try {
const response = await fetch(`${this.env.VPS_URL}/send-notification`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Secret": this.env.VPS_SECRET,
},
body: JSON.stringify({
type: "discord",
encrypted_webhook: encryptedWebhook,
message,
}),
});

if (!response.ok) {
const error = await response.text();
return { success: false, error };
}

return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
};
}
}

async sendTelegramNotification(
encryptedToken: string,
encryptedChatId: string,
message: NotificationMessage
): Promise<NotificationResult> {
try {
const response = await fetch(`${this.env.VPS_URL}/send-notification`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Secret": this.env.VPS_SECRET,
},
body: JSON.stringify({
type: "telegram",
encrypted_token: encryptedToken,
encrypted_chat_id: encryptedChatId,
message,
}),
});

if (!response.ok) {
const error = await response.text();
return { success: false, error };
}

return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
};
}
}
}












Security Considerations






1. End-to-End Encryption



Data flow:




  1. Credentials encrypted in D1 database

  2. Worker reads encrypted data (never decrypts)

  3. Worker sends encrypted data to Rust VPS

  4. Rust VPS decrypts and forwards

  5. Plaintext immediately discarded from memory



Even if HTTPS is compromised, credentials stay encrypted in transit.






2. API Authentication



Every request requires X-API-Secret header:




// In production, add middleware
async fn validate_api_secret(
headers: &HeaderMap,
) -> Result<(), StatusCode> {
let secret = headers
.get("X-API-Secret")
.and_then(|v| v.to_str().ok())
.ok_or(StatusCode::UNAUTHORIZED)?;

let expected_secret = std::env::var("VPS_SECRET")
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

if secret != expected_secret {
return Err(StatusCode::UNAUTHORIZED);
}

Ok(())
}









3. Stateless Design



The Rust VPS stores nothing:




  • No database

  • No file storage

  • No persistent state

  • Credentials decrypted, used, and immediately dropped



Memory safety guaranteed by Rust's ownership system.






4. Rate Limiting



Koyeb provides DDoS protection. For additional safety, add rate limiting:




use tower::limit::RateLimitLayer;
use std::time::Duration;

let app = Router::new()
.route("/send-notification", post(handlers::send_notification))
.layer(RateLimitLayer::new(100, Duration::from_secs(60))); // 100 req/min












Performance Results






Before (Direct from Workers)






Success rate: 0%
Error: 429 Too Many Requests
Latency: N/A (blocked)









After (Through Rust Proxy)






Success rate: 100%
Cold start: ~10 seconds (VPS sleeping)
Warm: ~3.6 seconds
Latency breakdown:
- Worker → VPS: ~500ms
- VPS decrypt: ~10ms
- VPS → Discord/Telegram: ~3s
- Total: ~3.6s









Binary Size






Rust binary: 5.2 MB
Docker image: 85 MB (with Debian base)
Memory usage: ~0 MB (idle)
CPU usage: <1% (idle)






Compare to Node.js:




  • Binary: 50+ MB

  • Docker image: 200+ MB

  • Memory: 50+ MB (idle)



Rust is perfect for resource-constrained environments.









Lessons Learned






1. Rust Has a Learning Curve



Coming from TypeScript, Rust's ownership system was challenging. But the compiler errors are incredibly helpful. Once it compiles, it usually works.






2. Axum Is Excellent



Ergonomic, fast, and well-documented. The middleware system is powerful. Highly recommend for Rust web services.






3. Encryption Is Tricky



Matching encryption algorithms between TypeScript and Rust required careful attention to:




  • IV/nonce size (12 bytes for GCM)

  • Base64 encoding (STANDARD vs URL_SAFE)

  • Key derivation (same 32-byte key)






4. Free Tiers Are Generous



Koyeb's free tier is perfect for this use case:




  • 512MB RAM (plenty for Rust)

  • Custom domains

  • Automatic HTTPS






5. Stateless Is Simpler



No database = no migrations, no backups, no consistency issues. Just pure compute. Rust's ownership system makes this safe.









What's Next?



In Part 3, I'll dive deep into the distributed queue system with Cloudflare Service Bindings:




  • Atomic queue claiming with SQL

  • Idempotency protection

  • Stale item requeuing

  • Batch progress tracking

  • Scaling to 1000+ users









Try It Out



Live App: streakyy.vercel.app



GitHub: github.com/0xReLogic/Streaky



Rust VPS Code: server/









Let's Connect



Building something similar? Have questions about Rust or Cloudflare Workers? Drop a comment!



GitHub: @0xReLogic



Project: Streaky

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Building Streaky: Solving Cloudflare IP Blocking with Rust (Part 2)

Thematisch verwandte Begriffe: Building, Streaky, Solving, Cloudflare · 6 Treffer

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-79918 | MaxKB is an open-source AI assistant for enterprise. Prior to version 2.…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick