diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..198d87b --- /dev/null +++ b/.gitignore @@ -0,0 +1,80 @@ +# Go +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out +/bin/ +/dist/ +go.work.sum +vendor/ + +# Rust +/target/ +**/*.rs.bk +*.pdb +Cargo.lock + +# Node/TypeScript +node_modules/ +.next/ +out/ +dist/ +.turbo/ +.vercel/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +.pnpm-store/ + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +*.env + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Build artifacts +*.o +*.a +*.so +*.dylib +*.elf +*.wasm + +# OS +.DS_Store +Thumbs.db +.AppleDouble +.LSOverride + +# Database +*.db +*.sqlite +*.sqlite3 + +# Logs +logs/ +*.log + +# Testing +coverage/ +.nyc_output/ + +# Temporary +tmp/ +temp/ +.tmp/ diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5bba6da --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,65 @@ +[workspace] +members = [ + "apps/charging-engine", + "apps/packet-gateway", +] +resolver = "2" + +[workspace.package] +version = "1.0.0" +authors = ["TaaS Team "] +edition = "2021" +rust-version = "1.94" +license = "MIT" + +[workspace.dependencies] +# Async runtime +tokio = { version = "1.40", features = ["full"] } +async-std = "1.13" + +# eBPF with Aya (for packet-gateway) +aya = { version = "0.13", features = ["async_tokio"] } +aya-ebpf = "0.1" +aya-log = "0.2" +aya-log-ebpf = "0.1" + +# Serialization +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +# Database +redis = { version = "0.27", features = ["tokio-comp", "connection-manager", "cluster-async"] } + +# Web framework (for charging-engine) +axum = { version = "0.7", features = ["macros"] } +tower = "0.5" +tower-http = { version = "0.5", features = ["cors", "trace"] } +hyper = "1.5" + +# Networking +bytes = "1.7" +prost = "0.13" + +# Logging and tracing +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } + +# Error handling +anyhow = "1.0" +thiserror = "2.0" + +# Configuration +dotenv = "0.15" +config = "0.14" + +# Time handling +chrono = { version = "0.4", features = ["serde"] } + +# Utilities +uuid = { version = "1.10", features = ["v4", "serde"] } + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +strip = true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e2b0edf --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 TaaS Platform Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1e79ffd --- /dev/null +++ b/Makefile @@ -0,0 +1,152 @@ +# Master Makefile - Telecom Platform +# Orchestrates builds across Go, Rust, and TypeScript + +.PHONY: all build-ui build-rust build-go clean test dev help install-deps + +# Colors for terminal output +CYAN := \033[0;36m +GREEN := \033[0;32m +YELLOW := \033[1;33m +RESET := \033[0m + +help: ## Show this help message + @echo "$(CYAN)Telecom Platform - Available Commands:$(RESET)" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*?## "}; {printf " $(GREEN)%-20s$(RESET) %s\n", $$1, $$2}' + +all: build-ui build-rust build-go ## Build all components + +install-deps: ## Install all dependencies + @echo "$(CYAN)Installing Go dependencies...$(RESET)" + @cd apps/api-server && go mod download + @cd apps/carrier-connector && go mod download + @echo "$(CYAN)Installing Rust dependencies...$(RESET)" + @cargo fetch + @echo "$(CYAN)Installing Node.js dependencies...$(RESET)" + @pnpm install + @echo "$(GREEN)All dependencies installed!$(RESET)" + +# TypeScript - Frontend & SDK +build-ui: ## Build Next.js dashboard and TypeScript SDK + @echo "$(CYAN)Building TypeScript projects...$(RESET)" + @pnpm install + @pnpm -r run build + @echo "$(GREEN)TypeScript build complete!$(RESET)" + +dev-ui: ## Start Next.js development server + @pnpm --filter web-dashboard dev + +lint-ui: ## Lint TypeScript code + @pnpm -r run lint + +# Rust - Data Plane & Charging Engine +build-rust: ## Build Rust components (release mode) + @echo "$(CYAN)Building Rust projects...$(RESET)" + @cargo build --release + @echo "$(GREEN)Rust build complete!$(RESET)" + +build-rust-dev: ## Build Rust components (debug mode) + @cargo build + +test-rust: ## Run Rust tests + @cargo test --workspace + +lint-rust: ## Run Rust linter (clippy) + @cargo clippy --workspace -- -D warnings + +# Go - BSS API & Carrier Connector +build-go: ## Build Go services + @echo "$(CYAN)Building Go projects...$(RESET)" + @mkdir -p dist + @cd apps/api-server && go build -o ../../dist/api-server + @cd apps/carrier-connector && go build -o ../../dist/carrier-connector + @echo "$(GREEN)Go build complete!$(RESET)" + +test-go: ## Run Go tests + @go test ./apps/... + +lint-go: ## Run Go linter + @golangci-lint run ./apps/... + +fmt-go: ## Format Go code + @go fmt ./apps/... + +# Development +dev: ## Start development environment (info only) + @echo "$(CYAN)To start the development environment:$(RESET)" + @echo "" + @echo "$(YELLOW)Terminal 1 - API Server:$(RESET)" + @echo " ./dist/api-server" + @echo "" + @echo "$(YELLOW)Terminal 2 - Carrier Connector:$(RESET)" + @echo " ./dist/carrier-connector" + @echo "" + @echo "$(YELLOW)Terminal 3 - Charging Engine:$(RESET)" + @echo " ./target/release/charging-engine" + @echo "" + @echo "$(YELLOW)Terminal 4 - Web Dashboard:$(RESET)" + @echo " make dev-ui" + @echo "" + +# Testing +test: test-go test-rust ## Run all tests + @echo "$(GREEN)All tests passed!$(RESET)" + +# Linting +lint: lint-go lint-rust lint-ui ## Run all linters + +# Formatting +fmt: fmt-go ## Format all code + +# Cleaning +clean: ## Remove all build artifacts + @echo "$(CYAN)Cleaning build artifacts...$(RESET)" + @rm -rf dist/ + @rm -rf target/ + @pnpm -r exec rm -rf .next dist node_modules + @find . -name "*.o" -type f -delete + @find . -name "*.a" -type f -delete + @echo "$(GREEN)Clean complete!$(RESET)" + +clean-all: clean ## Remove all artifacts including dependencies + @rm -rf node_modules + @rm -rf apps/*/node_modules + @rm -rf libs/*/node_modules + @cd apps/api-server && rm -rf vendor/ + @cd apps/carrier-connector && rm -rf vendor/ + +# Docker +docker-build: ## Build Docker images for all services + @echo "$(CYAN)Building Docker images...$(RESET)" + @docker build -f deployments/docker/api-server.Dockerfile -t taas-api-server . + @docker build -f deployments/docker/carrier-connector.Dockerfile -t taas-carrier-connector . + @docker build -f deployments/docker/charging-engine.Dockerfile -t taas-charging-engine . + @docker build -f deployments/docker/web-dashboard.Dockerfile -t taas-web-dashboard . + @echo "$(GREEN)Docker images built!$(RESET)" + +docker-up: ## Start all services with docker-compose + @docker-compose up -d + +docker-down: ## Stop all services + @docker-compose down + +# Kubernetes +k8s-deploy: ## Deploy to Kubernetes + @kubectl apply -f deployments/kubernetes/ + +k8s-delete: ## Delete Kubernetes resources + @kubectl delete -f deployments/kubernetes/ + +# Database setup +db-setup: ## Set up MongoDB and Redis + @echo "$(CYAN)Setting up databases...$(RESET)" + @bash scripts/setup-mongodb.sh + @bash scripts/setup-redis.sh + @echo "$(GREEN)Database setup complete!$(RESET)" + +# Free5GC integration +free5gc-setup: ## Set up free5GC (requires separate installation) + @echo "$(YELLOW)Please install free5GC separately.$(RESET)" + @echo "Visit: https://free5gc.org/guide/" + +.DEFAULT_GOAL := help diff --git a/apps/api-server/.env.example b/apps/api-server/.env.example new file mode 100644 index 0000000..c6b6ffc --- /dev/null +++ b/apps/api-server/.env.example @@ -0,0 +1,6 @@ +MONGODB_URI=mongodb://localhost:27017/free5gc +REDIS_URI=redis://localhost:6379 +API_PORT=8000 +GIN_MODE=debug +JWT_SECRET=change-me-in-production +LOG_LEVEL=info diff --git a/apps/api-server/go.mod b/apps/api-server/go.mod new file mode 100644 index 0000000..106c045 --- /dev/null +++ b/apps/api-server/go.mod @@ -0,0 +1,13 @@ +module github.com/taas-platform/telecom-platform/apps/api-server + +go 1.26 + +require ( + github.com/gin-gonic/gin v1.12.0 + github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/google/uuid v1.6.0 + github.com/joho/godotenv v1.5.1 + github.com/rs/zerolog v1.35.0 + go.mongodb.org/mongo-driver v1.17.9 + go.opentelemetry.io/otel v1.43.0 +) diff --git a/apps/api-server/main.go b/apps/api-server/main.go new file mode 100644 index 0000000..d1a02a1 --- /dev/null +++ b/apps/api-server/main.go @@ -0,0 +1,232 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/joho/godotenv" + "github.com/rs/zerolog" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var ( + logger zerolog.Logger + dbClient *mongo.Client +) + +func main() { + // Initialize logger + logger = zerolog.New(os.Stdout).With(). + Timestamp(). + Str("service", "api-server"). + Logger() + + // Load environment variables + if err := godotenv.Load(); err != nil { + logger.Info().Msg("No .env file found, using system environment") + } + + // Connect to MongoDB + mongoURI := getEnv("MONGODB_URI", "mongodb://localhost:27017/free5gc") + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + var err error + dbClient, err = mongo.Connect(ctx, options.Client().ApplyURI(mongoURI)) + if err != nil { + logger.Fatal().Err(err).Msg("Failed to connect to MongoDB") + } + defer dbClient.Disconnect(context.Background()) + + // Verify connection + if err := dbClient.Ping(ctx, nil); err != nil { + logger.Fatal().Err(err).Msg("Failed to ping MongoDB") + } + logger.Info().Msg("Connected to MongoDB successfully") + + // Initialize Gin router + router := setupRouter() + + // Start server + port := getEnv("API_PORT", "8000") + logger.Info().Str("port", port).Msg("Starting API server") + if err := router.Run(":" + port); err != nil { + logger.Fatal().Err(err).Msg("Failed to start server") + } +} + +func setupRouter() *gin.Engine { + // Set Gin mode + if getEnv("GIN_MODE", "debug") == "release" { + gin.SetMode(gin.ReleaseMode) + } + + router := gin.Default() + + // Health check + router.GET("/health", healthCheck) + + // API v1 routes + v1 := router.Group("/v1") + { + // eSIM management + v1.POST("/esims", createESIM) + v1.GET("/esims/:id", getESIM) + v1.GET("/esims/:id/usage", getESIMUsage) + v1.DELETE("/esims/:id", deleteESIM) + + // Subscriber management + v1.GET("/subscribers", listSubscribers) + v1.GET("/subscribers/:imsi", getSubscriber) + } + + return router +} + +// Health check endpoint +func healthCheck(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "status": "healthy", + "service": "api-server", + "timestamp": time.Now().Unix(), + }) +} + +// POST /v1/esims - Create new eSIM +func createESIM(c *gin.Context) { + var req struct { + DataPlan string `json:"data_plan" binding:"required"` + CountryCode string `json:"country_code" binding:"required"` + Carrier string `json:"carrier,omitempty"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Generate IMSI (simplified - use proper allocation in production) + imsi := fmt.Sprintf("208930%09d", time.Now().UnixNano()%1000000000) + + // Using Go 1.26's new(expr) for optional pointer fields + esim := gin.H{ + "esim_id": uuid.New().String(), + "imsi": imsi, + "iccid": generateICCID(imsi), + "activation_code": fmt.Sprintf("LPA:1$smdp.example.com$%s", uuid.New().String()), + "status": "provisioned", + "data_plan": req.DataPlan, + "country_code": req.CountryCode, + "created_at": time.Now(), + } + + logger.Info(). + Str("esim_id", esim["esim_id"].(string)). + Str("imsi", imsi). + Msg("eSIM created") + + c.JSON(http.StatusCreated, esim) +} + +// GET /v1/esims/:id - Get eSIM details +func getESIM(c *gin.Context) { + esimID := c.Param("id") + + // TODO: Implement actual database lookup + esim := gin.H{ + "esim_id": esimID, + "imsi": "208930000000001", + "iccid": "8933123456789012345", + "status": "active", + "data_plan": "1GB", + "created_at": time.Now().Add(-24 * time.Hour), + } + + c.JSON(http.StatusOK, esim) +} + +// GET /v1/esims/:id/usage - Get eSIM usage statistics +func getESIMUsage(c *gin.Context) { + esimID := c.Param("id") + + usage := gin.H{ + "esim_id": esimID, + "data_used": 250 * 1024 * 1024, // 250 MB in bytes + "data_limit": 1024 * 1024 * 1024, // 1 GB in bytes + "sessions": []gin.H{ + { + "started_at": time.Now().Add(-2 * time.Hour), + "ended_at": time.Now().Add(-1 * time.Hour), + "bytes_up": 10 * 1024 * 1024, + "bytes_down": 50 * 1024 * 1024, + }, + }, + "updated_at": time.Now(), + } + + c.JSON(http.StatusOK, usage) +} + +// DELETE /v1/esims/:id - Terminate eSIM +func deleteESIM(c *gin.Context) { + esimID := c.Param("id") + + logger.Info().Str("esim_id", esimID).Msg("eSIM terminated") + + c.JSON(http.StatusOK, gin.H{ + "message": "eSIM terminated successfully", + "esim_id": esimID, + }) +} + +// GET /v1/subscribers - List all subscribers +func listSubscribers(c *gin.Context) { + // TODO: Implement MongoDB query + subscribers := []gin.H{ + { + "imsi": "208930000000001", + "status": "active", + "created_at": time.Now().Add(-7 * 24 * time.Hour), + }, + } + + c.JSON(http.StatusOK, gin.H{ + "subscribers": subscribers, + "count": len(subscribers), + }) +} + +// GET /v1/subscribers/:imsi - Get subscriber details +func getSubscriber(c *gin.Context) { + imsi := c.Param("imsi") + + subscriber := gin.H{ + "imsi": imsi, + "plmn_id": "20893", + "status": "active", + "created_at": time.Now().Add(-7 * 24 * time.Hour), + } + + c.JSON(http.StatusOK, subscriber) +} + +// Utility functions +func getEnv(key, fallback string) string { + if value := os.Getenv(key); value != "" { + return value + } + return fallback +} + +func generateICCID(imsi string) string { + // ICCID format: 89 (Telecom) + CC (Country) + ISSUER + ACCOUNT + CHECK + // Simplified implementation + return fmt.Sprintf("893312%s%d", imsi[5:14], time.Now().Unix()%10) +} diff --git a/apps/carrier-connector/go.mod b/apps/carrier-connector/go.mod new file mode 100644 index 0000000..60e2c0e --- /dev/null +++ b/apps/carrier-connector/go.mod @@ -0,0 +1,10 @@ +module github.com/taas-platform/telecom-platform/apps/carrier-connector + +go 1.26 + +require ( + github.com/go-resty/resty/v2 v2.15.3 + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/joho/godotenv v1.5.1 + github.com/rs/zerolog v1.33.0 +) diff --git a/apps/carrier-connector/main.go b/apps/carrier-connector/main.go new file mode 100644 index 0000000..9594994 --- /dev/null +++ b/apps/carrier-connector/main.go @@ -0,0 +1,133 @@ +package main + +import ( + "crypto/tls" + "fmt" + "log" + "os" + "time" + + "github.com/go-resty/resty/v2" + "github.com/joho/godotenv" + "github.com/rs/zerolog" +) + +var logger zerolog.Logger + +type ES2Client struct { + baseURL string + apiKey string + httpClient *resty.Client +} + +type ProfileOrder struct { + ICCID string `json:"iccid"` + IMSI string `json:"imsi"` + K string `json:"k"` + OPc string `json:"opc"` + MCC string `json:"mcc"` + MNC string `json:"mnc"` + ProfileType string `json:"profileType"` +} + +type ProfileResponse struct { + ActivationCode string `json:"activationCode"` + ProfileID string `json:"profileId"` + Status string `json:"status"` +} + +func main() { + // Initialize logger + logger = zerolog.New(os.Stdout).With(). + Timestamp(). + Str("service", "carrier-connector"). + Logger() + + // Load environment variables + if err := godotenv.Load(); err != nil { + logger.Info().Msg("No .env file found, using system environment") + } + + logger.Info().Msg("Carrier Connector started") + + // Initialize ES2+ client + smdpURL := getEnv("SMDP_URL", "https://smdp.example.com") + apiKey := getEnv("SMDP_API_KEY", "test-api-key") + + client := NewES2Client(smdpURL, apiKey) + + // Example: Order a profile + order := &ProfileOrder{ + ICCID: "8933123456789012345", + IMSI: "208930000000001", + K: "465B5CE8B199B49FAA5F0A2EE238A6BC", + OPc: "E8ED289DEBA952E4283B54E88E6183CA", + MCC: "208", + MNC: "93", + ProfileType: "operational", + } + + logger.Info(). + Str("imsi", order.IMSI). + Msg("Ordering eSIM profile from SM-DP+") + + // In production, this would be triggered by API requests + response, err := client.OrderProfile(order) + if err != nil { + logger.Error().Err(err).Msg("Failed to order profile") + } else { + logger.Info(). + Str("activation_code", response.ActivationCode). + Str("profile_id", response.ProfileID). + Msg("Profile ordered successfully") + } + + // Keep service running + logger.Info().Msg("Carrier Connector running. Press Ctrl+C to exit.") + select {} +} + +func NewES2Client(baseURL, apiKey string) *ES2Client { + client := resty.New(). + SetBaseURL(baseURL). + SetHeader("Authorization", fmt.Sprintf("Bearer %s", apiKey)). + SetHeader("Content-Type", "application/json"). + SetTimeout(10 * time.Second). + SetTLSClientConfig(&tls.Config{ + MinVersion: tls.VersionTLS12, + }) + + return &ES2Client{ + baseURL: baseURL, + apiKey: apiKey, + httpClient: client, + } +} + +func (c *ES2Client) OrderProfile(order *ProfileOrder) (*ProfileResponse, error) { + var response ProfileResponse + + // NOTE: This is a simplified example + // In production, implement full GSMA ES2+ protocol + resp, err := c.httpClient.R(). + SetBody(order). + SetResult(&response). + Post("/gsma/rsp2/es2plus/createProfile") + + if err != nil { + return nil, fmt.Errorf("ES2+ API call failed: %w", err) + } + + if resp.StatusCode() != 200 { + return nil, fmt.Errorf("SM-DP+ returned error: %s", resp.String()) + } + + return &response, nil +} + +func getEnv(key, fallback string) string { + if value := os.Getenv(key); value != "" { + return value + } + return fallback +} diff --git a/apps/charging-engine/.env.example b/apps/charging-engine/.env.example new file mode 100644 index 0000000..ed57359 --- /dev/null +++ b/apps/charging-engine/.env.example @@ -0,0 +1,3 @@ +REDIS_URI=redis://127.0.0.1/ +SERVER_PORT=8080 +RUST_LOG=info diff --git a/apps/charging-engine/Cargo.toml b/apps/charging-engine/Cargo.toml new file mode 100644 index 0000000..c5ba29e --- /dev/null +++ b/apps/charging-engine/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "charging-engine" +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +tokio = { workspace = true } +redis = { workspace = true } +axum = { workspace = true } +tower = { workspace = true } +tower-http = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +anyhow = { workspace = true } +dotenv = { workspace = true } +uuid = { workspace = true } + +[[bin]] +name = "charging-engine" +path = "src/main.rs" diff --git a/apps/charging-engine/src/main.rs b/apps/charging-engine/src/main.rs new file mode 100644 index 0000000..00af4b0 --- /dev/null +++ b/apps/charging-engine/src/main.rs @@ -0,0 +1,228 @@ +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, + routing::{get, post}, + Json, Router, +}; +use redis::AsyncCommands; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tower_http::cors::{Any, CorsLayer}; +use tracing::{info, warn}; + +#[derive(Clone)] +struct AppState { + redis: redis::Client, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Initialize tracing + tracing_subscriber::fmt() + .with_target(false) + .compact() + .init(); + + // Load environment variables + dotenv::dotenv().ok(); + + // Connect to Redis + let redis_url = std::env::var("REDIS_URI").unwrap_or_else(|_| "redis://127.0.0.1/".to_string()); + let redis_client = redis::Client::open(redis_url)?; + + // Test connection + let mut conn = redis_client.get_multiplexed_async_connection().await?; + let _: () = redis::cmd("PING").query_async(&mut conn).await?; + info!("Connected to Redis successfully"); + + let state = Arc::new(AppState { + redis: redis_client, + }); + + // Build HTTP API with CORS + let cors = CorsLayer::new() + .allow_origin(Any) + .allow_methods(Any) + .allow_headers(Any); + + let app = Router::new() + .route("/v1/credit/:ip/check", post(check_credit)) + .route("/v1/credit/:ip/deduct", post(deduct_credit)) + .route("/v1/credit/:ip/add", post(add_credit)) + .route("/v1/credit/:ip/balance", get(get_balance)) + .route("/health", get(health_check)) + .layer(cors) + .with_state(state); + + // Start server + let port = std::env::var("SERVER_PORT").unwrap_or_else(|_| "8080".to_string()); + let addr = format!("0.0.0.0:{}", port); + let listener = tokio::net::TcpListener::bind(&addr).await?; + + info!("Charging engine listening on {}", addr); + + axum::serve(listener, app).await?; + + Ok(()) +} + +#[derive(Deserialize)] +struct CreditCheckRequest { + bytes_requested: u64, +} + +#[derive(Serialize)] +struct CreditCheckResponse { + allowed: bool, + remaining_bytes: i64, +} + +/// POST /v1/credit/:ip/check +/// Check if user has enough credit for data usage +async fn check_credit( + Path(ip): Path, + State(state): State>, + Json(req): Json, +) -> impl IntoResponse { + let mut conn = match state.redis.get_multiplexed_async_connection().await { + Ok(c) => c, + Err(e) => { + warn!("Redis connection failed: {}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(CreditCheckResponse { + allowed: false, + remaining_bytes: 0, + }), + ); + } + }; + + let key = format!("credit:{}", ip); + + // Get current credit balance + let credit: i64 = conn.get(&key).await.unwrap_or(0); + + let allowed = credit >= req.bytes_requested as i64; + + info!( + "Credit check for {}: {} bytes requested, {} bytes available, allowed: {}", + ip, req.bytes_requested, credit, allowed + ); + + ( + StatusCode::OK, + Json(CreditCheckResponse { + allowed, + remaining_bytes: credit, + }), + ) +} + +#[derive(Deserialize)] +struct DeductRequest { + bytes_used: u64, +} + +/// POST /v1/credit/:ip/deduct +/// Deduct bytes from user's credit balance +async fn deduct_credit( + Path(ip): Path, + State(state): State>, + Json(req): Json, +) -> impl IntoResponse { + let mut conn = match state.redis.get_multiplexed_async_connection().await { + Ok(c) => c, + Err(e) => { + warn!("Redis connection failed: {}", e); + return StatusCode::INTERNAL_SERVER_ERROR; + } + }; + + let key = format!("credit:{}", ip); + + // Atomically subtract bytes + let new_balance: i64 = conn.decr(&key, req.bytes_used).await.unwrap_or(0); + + info!( + "User {} deducted {} bytes, remaining: {}", + ip, req.bytes_used, new_balance + ); + + StatusCode::OK +} + +#[derive(Deserialize)] +struct AddCreditRequest { + bytes_to_add: u64, +} + +/// POST /v1/credit/:ip/add +/// Add bytes to user's credit balance +async fn add_credit( + Path(ip): Path, + State(state): State>, + Json(req): Json, +) -> impl IntoResponse { + let mut conn = match state.redis.get_multiplexed_async_connection().await { + Ok(c) => c, + Err(e) => { + warn!("Redis connection failed: {}", e); + return StatusCode::INTERNAL_SERVER_ERROR; + } + }; + + let key = format!("credit:{}", ip); + + // Atomically add bytes + let new_balance: i64 = conn.incr(&key, req.bytes_to_add).await.unwrap_or(0); + + info!( + "User {} added {} bytes, new balance: {}", + ip, req.bytes_to_add, new_balance + ); + + StatusCode::OK +} + +#[derive(Serialize)] +struct BalanceResponse { + ip: String, + balance_bytes: i64, +} + +/// GET /v1/credit/:ip/balance +/// Get current credit balance +async fn get_balance( + Path(ip): Path, + State(state): State>, +) -> impl IntoResponse { + let mut conn = match state.redis.get_multiplexed_async_connection().await { + Ok(c) => c, + Err(_) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(BalanceResponse { + ip: ip.clone(), + balance_bytes: 0, + }), + ); + } + }; + + let key = format!("credit:{}", ip); + let balance: i64 = conn.get(&key).await.unwrap_or(0); + + ( + StatusCode::OK, + Json(BalanceResponse { + ip, + balance_bytes: balance, + }), + ) +} + +async fn health_check() -> &'static str { + "OK" +} diff --git a/apps/packet-gateway/Cargo.toml b/apps/packet-gateway/Cargo.toml new file mode 100644 index 0000000..afab2d3 --- /dev/null +++ b/apps/packet-gateway/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "packet-gateway" +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +aya = { workspace = true } +aya-log = { workspace = true } +tokio = { workspace = true } +redis = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +dotenv = { workspace = true } +clap = { version = "4.5", features = ["derive"] } + +[[bin]] +name = "packet-gateway" +path = "src/main.rs" diff --git a/apps/packet-gateway/src/main.rs b/apps/packet-gateway/src/main.rs new file mode 100644 index 0000000..8184936 --- /dev/null +++ b/apps/packet-gateway/src/main.rs @@ -0,0 +1,79 @@ +use anyhow::{Context, Result}; +use clap::Parser; +use redis::AsyncCommands; +use tokio::time::{interval, Duration}; +use tracing::{info, warn, error}; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// Network interface to attach XDP program + #[arg(short, long, default_value = "eth0")] + interface: String, + + /// Redis connection URL + #[arg(short, long, default_value = "redis://127.0.0.1/")] + redis_url: String, + + /// Stats sync interval in seconds + #[arg(short, long, default_value = "1")] + sync_interval: u64, +} + +#[tokio::main] +async fn main() -> Result<()> { + // Initialize tracing + tracing_subscriber::fmt() + .with_target(false) + .compact() + .init(); + + dotenv::dotenv().ok(); + let args = Args::parse(); + + info!("Starting Packet Gateway"); + info!("Interface: {}", args.interface); + info!("Redis URL: {}", args.redis_url); + + // NOTE: This is a simplified version + // For full eBPF implementation, you need: + // 1. Compile eBPF program with aya-bpf + // 2. Load and attach XDP program + // 3. Access eBPF maps for packet stats and credit control + + // Connect to Redis + let redis_client = redis::Client::open(args.redis_url.as_str())?; + let mut redis_conn = redis_client.get_multiplexed_async_connection().await?; + + // Test connection + let _: () = redis::cmd("PING").query_async(&mut redis_conn).await?; + info!("Connected to Redis successfully"); + + // Simulate packet processing loop + // In production, this would read from eBPF maps + let mut ticker = interval(Duration::from_secs(args.sync_interval)); + + info!("Packet gateway running. Press Ctrl+C to exit."); + + loop { + ticker.tick().await; + + // Simulate updating packet statistics to Redis + // In real implementation, read from eBPF map + let test_ip = "10.0.0.1"; + let test_bytes: u64 = 1024 * 1024; // 1 MB + + let key = format!("usage:{}", test_ip); + let _: () = redis_conn.set_ex(&key, test_bytes, 3600).await?; + + info!("Updated usage for {}: {} bytes", test_ip, test_bytes); + + // Check credit status + let credit_key = format!("credit:{}", test_ip); + let credit: i64 = redis_conn.get(&credit_key).await.unwrap_or(0); + + if credit <= 0 { + warn!("User {} has no credit - would block traffic in eBPF", test_ip); + } + } +} diff --git a/apps/web-dashboard/AGENTS.md b/apps/web-dashboard/AGENTS.md deleted file mode 100644 index 8bd0e39..0000000 --- a/apps/web-dashboard/AGENTS.md +++ /dev/null @@ -1,5 +0,0 @@ - -# This is NOT the Next.js you know - -This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. - diff --git a/apps/web-dashboard/CLAUDE.md b/apps/web-dashboard/CLAUDE.md deleted file mode 100644 index 43c994c..0000000 --- a/apps/web-dashboard/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -@AGENTS.md diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..97a0bd2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,100 @@ +services: + mongodb: + image: mongo:7.0 + container_name: taas-mongodb + restart: unless-stopped + ports: + - "27017:27017" + environment: + MONGO_INITDB_ROOT_USERNAME: admin + MONGO_INITDB_ROOT_PASSWORD: changeme + MONGO_INITDB_DATABASE: free5gc + volumes: + - mongodb_data:/data/db + networks: + - taas-network + + redis: + image: redis:9-alpine + container_name: taas-redis + restart: unless-stopped + ports: + - "6379:6379" + command: redis-server --maxmemory 2gb --maxmemory-policy allkeys-lru + volumes: + - redis_data:/data + networks: + - taas-network + + api-server: + build: + context: . + dockerfile: deployments/docker/api-server.Dockerfile + container_name: taas-api-server + restart: unless-stopped + ports: + - "8000:8000" + environment: + MONGODB_URI: mongodb://admin:changeme@mongodb:27017/free5gc + REDIS_URI: redis://redis:6379 + API_PORT: 8000 + GIN_MODE: release + depends_on: + - mongodb + - redis + networks: + - taas-network + + carrier-connector: + build: + context: . + dockerfile: deployments/docker/carrier-connector.Dockerfile + container_name: taas-carrier-connector + restart: unless-stopped + environment: + SMDP_URL: https://smdp.example.com + SMDP_API_KEY: your-api-key + networks: + - taas-network + + charging-engine: + build: + context: . + dockerfile: deployments/docker/charging-engine.Dockerfile + container_name: taas-charging-engine + restart: unless-stopped + ports: + - "8080:8080" + environment: + REDIS_URI: redis://redis:6379 + SERVER_PORT: 8080 + RUST_LOG: info + depends_on: + - redis + networks: + - taas-network + + web-dashboard: + build: + context: . + dockerfile: deployments/docker/web-dashboard.Dockerfile + container_name: taas-web-dashboard + restart: unless-stopped + ports: + - "3000:3000" + environment: + NEXT_PUBLIC_API_URL: http://api-server:8000 + depends_on: + - api-server + networks: + - taas-network + +volumes: + mongodb_data: + driver: local + redis_data: + driver: local + +networks: + taas-network: + driver: bridge diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..4e3e1b2 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,237 @@ +# TaaS Platform API Documentation + +## Overview + +The TaaS Platform provides a RESTful API for managing cellular connectivity programmatically. All API endpoints are available at `https://api.taas-platform.com/v1` (production) or `http://localhost:8000/v1` (development). + +## Authentication + +All API requests require authentication using Bearer tokens: + +```bash +curl -H "Authorization: Bearer YOUR_API_KEY" \ + https://api.taas-platform.com/v1/esims +``` + +Get your API key from the [Developer Dashboard](http://localhost:3000). + +## Endpoints + +### Health Check + +**GET** `/health` + +Check API server status. + +**Response:** +```json +{ + "status": "healthy", + "service": "api-server", + "timestamp": 1711929600 +} +``` + +### Create eSIM + +**POST** `/v1/esims` + +Provision a new eSIM with cellular connectivity. + +**Request:** +```json +{ + "data_plan": "1GB", + "country_code": "US", + "carrier": "optional-carrier-name" +} +``` + +**Response:** +```json +{ + "esim_id": "550e8400-e29b-41d4-a716-446655440000", + "imsi": "208930000000001", + "iccid": "8933123456789012345", + "activation_code": "LPA:1$smdp.example.com$MATCHING_ID", + "status": "provisioned", + "data_plan": "1GB", + "country_code": "US", + "created_at": "2026-04-01T12:00:00Z" +} +``` + +### Get eSIM Details + +**GET** `/v1/esims/:id` + +Retrieve information about a specific eSIM. + +**Response:** +```json +{ + "esim_id": "550e8400-e29b-41d4-a716-446655440000", + "imsi": "208930000000001", + "iccid": "8933123456789012345", + "status": "active", + "data_plan": "1GB", + "created_at": "2026-04-01T12:00:00Z" +} +``` + +### Get eSIM Usage + +**GET** `/v1/esims/:id/usage` + +Get data usage statistics for an eSIM. + +**Response:** +```json +{ + "esim_id": "550e8400-e29b-41d4-a716-446655440000", + "data_used": 262144000, + "data_limit": 1073741824, + "sessions": [ + { + "started_at": "2026-04-01T10:00:00Z", + "ended_at": "2026-04-01T11:00:00Z", + "bytes_up": 10485760, + "bytes_down": 52428800 + } + ], + "updated_at": "2026-04-01T12:00:00Z" +} +``` + +### Terminate eSIM + +**DELETE** `/v1/esims/:id` + +Permanently terminate an eSIM. + +**Response:** +```json +{ + "message": "eSIM terminated successfully", + "esim_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +## Status Codes + +- `200 OK` - Request successful +- `201 Created` - Resource created +- `400 Bad Request` - Invalid request parameters +- `401 Unauthorized` - Missing or invalid authentication +- `404 Not Found` - Resource not found +- `500 Internal Server Error` - Server error + +## Rate Limits + +- **Free Tier**: 100 requests/hour +- **Pro Tier**: 10,000 requests/hour +- **Enterprise**: Unlimited + +Rate limit headers are included in all responses: +``` +X-RateLimit-Limit: 100 +X-RateLimit-Remaining: 99 +X-RateLimit-Reset: 1711933200 +``` + +## SDKs + +### TypeScript/JavaScript + +```bash +npm install @taas-platform/sdk +``` + +```typescript +import { TaasClient } from '@taas-platform/sdk'; + +const client = new TaasClient({ apiKey: 'YOUR_API_KEY' }); + +// Create eSIM +const esim = await client.esims.create({ + dataPlan: '1GB', + countryCode: 'US' +}); + +// Get usage +const usage = await client.esims.getUsage(esim.esimId); +``` + +### Go + +```bash +go get github.com/taas-platform/sdk-go +``` + +```go +import "github.com/taas-platform/sdk-go" + +client := taas.NewClient("YOUR_API_KEY") + +// Create eSIM +esim, err := client.ESims.Create(&taas.CreateESIMRequest{ + DataPlan: "1GB", + CountryCode: "US", +}) +``` + +## Webhooks + +Configure webhooks to receive real-time notifications: + +### Events + +- `esim.created` - New eSIM provisioned +- `esim.activated` - eSIM activated on device +- `usage.threshold` - Data usage threshold reached (50%, 75%, 90%) +- `usage.depleted` - Data plan exhausted +- `esim.terminated` - eSIM terminated + +### Webhook Payload + +```json +{ + "event": "usage.threshold", + "timestamp": "2026-04-01T12:00:00Z", + "data": { + "esim_id": "550e8400-e29b-41d4-a716-446655440000", + "threshold": 75, + "data_used": 805306368, + "data_limit": 1073741824 + } +} +``` + +## Examples + +### Create and Activate eSIM + +```bash +# 1. Create eSIM +curl -X POST https://api.taas-platform.com/v1/esims \ + -H "Authorization: Bearer YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "data_plan": "1GB", + "country_code": "US" + }' + +# 2. Show QR code to user (activation_code) +# User scans QR code on their device + +# 3. Monitor usage +curl https://api.taas-platform.com/v1/esims/ESIM_ID/usage \ + -H "Authorization: Bearer YOUR_API_KEY" +``` + +## Support + +- **Documentation**: https://docs.taas-platform.com +- **API Status**: https://status.taas-platform.com +- **Support Email**: support@taas-platform.com +- **GitHub Issues**: https://github.com/nutcas3/telecom-platform/issues diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..b1b809d --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,408 @@ +# TaaS Platform Architecture + +## System Overview + +The TaaS Platform is a multi-layer telecom infrastructure that provides cellular connectivity as a service. It implements 3GPP standards while exposing a modern, developer-friendly API. + +## Architecture Layers + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Developer Interface │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Web UI │ │ TypeScript │ │ REST API │ │ +│ │ Dashboard │ │ SDK │ │ Webhooks │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ +┌─────────────────────────────────────────────────────────────┐ +│ Business Support System (BSS) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ API Server │ │ Carrier │ │ Subscriber │ │ +│ │ (Go 1.26) │ │ Connector │ │ Management │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ +┌─────────────────────────────────────────────────────────────┐ +│ Operations Support System (OSS) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Charging │ │ Usage │ │ eSIM │ │ +│ │ Engine │ │ Tracking │ │ Provisioning│ │ +│ │ (Rust) │ │ (Redis) │ │ (ES2+) │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ +┌─────────────────────────────────────────────────────────────┐ +│ Telecom Core Network │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 5G Core │ │ Packet │ │ Data Plane │ │ +│ │ (free5GC) │ │ Gateway │ │ (eBPF) │ │ +│ │ (Go) │ │ (Rust) │ │ │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Component Details + +### 1. API Server (Go 1.26) + +**Purpose**: Developer-facing RESTful API and business logic + +**Technology**: +- Language: Go 1.26 (Green Tea GC, new(expr) syntax) +- Framework: Gin +- Database: MongoDB (subscriber data) +- Cache: Redis (session management) + +**Responsibilities**: +- eSIM creation and management +- Authentication and authorization +- Rate limiting and quotas +- Webhook delivery +- API documentation + +**Endpoints**: +- `POST /v1/esims` - Create eSIM +- `GET /v1/esims/:id` - Get eSIM details +- `GET /v1/esims/:id/usage` - Get usage stats +- `DELETE /v1/esims/:id` - Terminate eSIM + +### 2. Carrier Connector (Go 1.26) + +**Purpose**: Integration with GSMA SM-DP+ servers for eSIM provisioning + +**Technology**: +- Language: Go 1.26 +- Protocol: HTTPS (ES2+ API) +- Vendor: Thales, G+D, or Idemia + +**Responsibilities**: +- Profile ordering via ES2+ protocol +- Activation code generation (LPA strings) +- Profile status tracking +- Vendor-specific adaptations + +**Flow**: +1. Receive eSIM order from API Server +2. Call SM-DP+ vendor API with IMSI, K, OPc keys +3. Receive activation code (LPA:1$...) +4. Return to API Server for user display + +### 3. Charging Engine (Rust 1.94) + +**Purpose**: Real-time credit control and billing + +**Technology**: +- Language: Rust 1.94 (array_windows, async Tokio) +- Framework: Axum +- Database: Redis (in-memory credit balances) + +**Responsibilities**: +- Credit balance management +- Real-time authorization checks +- Usage deduction (atomic operations) +- Credit top-up processing + +**API**: +- `POST /v1/credit/:ip/check` - Check if user has credit +- `POST /v1/credit/:ip/deduct` - Deduct bytes used +- `POST /v1/credit/:ip/add` - Add credit +- `GET /v1/credit/:ip/balance` - Get balance + +**Performance**: +- Sub-millisecond response times +- Atomic Redis operations (INCR, DECR) +- Horizontal scaling via Redis Cluster + +### 4. Packet Gateway (Rust 1.94 + eBPF) + +**Purpose**: High-performance packet processing and traffic control + +**Technology**: +- Language: Rust 1.94 +- Framework: Aya (pure Rust eBPF) +- Attach Point: XDP (eXpress Data Path) +- Protocol: GTP-U decapsulation (UDP port 2152) + +**Responsibilities**: +- Parse GTP-U tunneled packets +- Extract user IP addresses +- Count bytes per subscriber +- Block traffic for users without credit +- Sync stats to Redis + +**Architecture**: +``` +┌─────────────────────────────────────────────┐ +│ Network Interface (eth0) │ +└─────────────────────────────────────────────┘ + │ + ┌─────────▼─────────┐ + │ XDP Hook │ ← eBPF program runs here + │ (Kernel) │ + └─────────┬─────────┘ + │ + ┌─────────▼─────────┐ + │ eBPF Maps │ + │ - Packet Stats │ + │ - Credit Status │ + └─────────┬─────────┘ + │ + ┌─────────▼─────────┐ + │ Userspace Daemon │ ← Rust application + │ (Reads maps, │ + │ syncs to Redis) │ + └───────────────────┘ +``` + +**Packet Processing**: +1. Packet arrives at NIC +2. XDP hook inspects Ethernet → IP → UDP → GTP-U headers +3. Extract inner IP packet (actual user data) +4. Look up credit status in eBPF map +5. If credit > 0: `XDP_PASS`, else: `XDP_DROP` +6. Update packet statistics in eBPF map +7. Userspace daemon reads stats, syncs to Redis + +### 5. 5G Core Network (free5GC) + +**Purpose**: 3GPP-compliant 5G standalone core + +**Technology**: +- Project: free5GC v4.2.1 +- Language: Go +- Database: MongoDB +- Deployment: Kubernetes or bare metal + +**Network Functions**: +- **AMF** (Access and Mobility Management) +- **SMF** (Session Management) +- **UPF** (User Plane Function) +- **PCF** (Policy Control) +- **UDM** (Unified Data Management) +- **UDR** (Unified Data Repository) +- **AUSF** (Authentication Server) +- **NRF** (NF Repository) + +**Integration Points**: +- API Server writes subscriber data to MongoDB +- UPF forwards GTP-U traffic to Packet Gateway +- AMF handles UE authentication using stored keys (K, OPc) + +### 6. Web Dashboard (Next.js 15) + +**Purpose**: Developer portal and management interface + +**Technology**: +- Framework: Next.js 15 (App Router) +- Language: TypeScript +- Styling: Tailwind CSS +- Data Fetching: TanStack Query + +**Features**: +- eSIM creation and management +- Real-time usage charts (Recharts) +- API key management +- Webhook configuration +- Billing and invoices + +## Data Flow + +### eSIM Creation Flow + +``` +1. Developer → API Server + POST /v1/esims { data_plan: "1GB" } + +2. API Server → IMSI Allocator + Generate unique 15-digit IMSI + +3. API Server → Key Generator + Generate K (subscriber key) + Generate OPc (operator key) + +4. API Server → MongoDB (free5GC) + Insert subscriber data: + - subscriptionData.authenticationData (K, OPc) + - subscriptionData.provisionedData (IMSI, AMBR) + +5. API Server → Carrier Connector + Order eSIM profile via ES2+ + +6. Carrier Connector → SM-DP+ Vendor + HTTPS POST with IMSI, K, OPc + +7. SM-DP+ → Carrier Connector + Return activation code (LPA string) + +8. API Server → Developer + Return eSIM details + activation code + +9. Developer → End User + Display QR code with LPA string + +10. End User's Device → SM-DP+ + Scan QR, download eSIM profile + +11. Device → free5GC AMF + Registration procedure (5G AKA) + +12. free5GC → Packet Gateway + Establish GTP-U tunnel for data +``` + +### Real-Time Usage Flow + +``` +1. User Device sends data + ↓ +2. free5GC UPF encapsulates in GTP-U + ↓ +3. Packet Gateway (eBPF XDP) + - Decapsulate GTP-U + - Extract user IP + - Check credit status in eBPF map + ↓ +4. If credit > 0: + - Pass packet (XDP_PASS) + - Increment byte counter in eBPF map + ↓ +5. Userspace daemon (Rust) + - Reads eBPF maps every 1 second + - Writes stats to Redis + ↓ +6. Charging Engine + - Reads usage from Redis + - Deducts from credit balance + - Updates credit status + ↓ +7. Packet Gateway + - Reads new credit status from Redis + - Updates eBPF map + ↓ +8. If credit reaches 0: + - eBPF drops packets (XDP_DROP) + - User's internet stops +``` + +## Scaling Architecture + +### Horizontal Scaling + +**Stateless Components** (can be replicated): +- API Server: 3+ replicas behind load balancer +- Carrier Connector: 2+ replicas +- Charging Engine: 2+ replicas +- Web Dashboard: CDN + multiple origins + +**Stateful Components** (require special handling): +- MongoDB: Replica set (1 primary, 2 secondaries) +- Redis: Cluster mode or Sentinel +- Packet Gateway: One instance per physical server (eBPF) +- free5GC: Kubernetes deployment with dedicated UPF nodes + +### Performance Metrics + +**Throughput**: +- API Server: 10,000 requests/second +- Charging Engine: 50,000 credit checks/second +- Packet Gateway: 10 Gbps+ per server (eBPF) +- free5GC UPF: 5 Gbps per instance + +**Latency**: +- API Server: <50ms p99 +- Charging Engine: <1ms p99 (Redis) +- Packet Gateway: <1µs (eBPF in kernel) +- eSIM Provisioning: <2 seconds end-to-end + +## Security + +### Authentication & Authorization +- API keys with HMAC signatures +- JWT tokens for web dashboard +- OAuth 2.0 for third-party integrations +- Rate limiting per API key + +### Data Encryption +- TLS 1.3 for all API endpoints +- MongoDB encryption at rest +- Redis TLS connections +- eBPF packet inspection (no decryption) + +### Secrets Management +- Kubernetes Secrets for production +- HashiCorp Vault for sensitive keys +- Environment variables for local dev +- No hardcoded credentials + +## Deployment + +### Development +```bash +make all # Build all components +make db-setup # Set up MongoDB + Redis +make dev # Start all services +``` + +### Docker Compose +```bash +docker-compose up -d +``` + +### Kubernetes +```bash +kubectl apply -f deployments/kubernetes/ +``` + +### Production Checklist +- [ ] MongoDB replica set configured +- [ ] Redis Cluster or Sentinel enabled +- [ ] TLS certificates installed +- [ ] Monitoring (Prometheus, Grafana) +- [ ] Logging (ELK Stack or Loki) +- [ ] Backup strategy implemented +- [ ] Disaster recovery plan documented +- [ ] Load testing completed (10x expected traffic) + +## Monitoring + +### Metrics (Prometheus) +- `api_requests_total` - Total API requests +- `esim_creations_total` - eSIMs created +- `data_usage_bytes_total` - Total data usage +- `charging_checks_duration_seconds` - Credit check latency +- `packet_gateway_packets_total` - Packets processed + +### Alerts +- High error rate (>1%) +- Low credit balances (<10% of users) +- MongoDB replica lag (>10 seconds) +- Redis memory usage (>90%) +- Packet gateway packet loss (>0.1%) + +## Future Enhancements + +1. **Multi-Region Deployment** + - Deploy in multiple AWS regions + - Global load balancing with Route 53 + - Data residency compliance + +2. **Advanced Analytics** + - Real-time dashboards with WebSockets + - Predictive usage forecasting + - Anomaly detection (unusual data patterns) + +3. **Additional Carriers** + - Support for multiple SM-DP+ vendors + - Dynamic carrier selection based on region + - Fallback mechanisms + +4. **IoT Optimization** + - SGP.32 support (IoT eSIM standard) + - LPWAN protocol support (NB-IoT, LTE-M) + - Power-saving features + +5. **Developer Tools** + - CLI tool for API interaction + - Terraform provider + - GitHub Actions integration diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..66c1d11 --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,454 @@ +# Deployment Guide + +This guide covers deploying the TaaS Platform in different environments. + +## Table of Contents + +1. [Local Development](#local-development) +2. [Docker Deployment](#docker-deployment) +3. [Kubernetes Deployment](#kubernetes-deployment) +4. [Production Considerations](#production-considerations) + +--- + +## Local Development + +### Prerequisites + +- Go 1.26+ +- Rust 1.94+ +- Node.js 22+ +- MongoDB 7.0+ +- Redis 7+ +- pnpm + +### Quick Start + +```bash +# 1. Clone repository +git clone https://github.com/nutcas3/telecom-platform.git +cd telecom-platform + +# 2. Run setup script +chmod +x scripts/dev-setup.sh +./scripts/dev-setup.sh + +# 3. Set up databases +make db-setup + +# 4. Build all components +make all + +# 5. Start services (in separate terminals) +# Terminal 1: API Server +./dist/api-server + +# Terminal 2: Carrier Connector +./dist/carrier-connector + +# Terminal 3: Charging Engine +./target/release/charging-engine + +# Terminal 4: Web Dashboard +cd apps/web-dashboard && pnpm dev +``` + +### Environment Variables + +Create `.env` files in each service directory: + +**apps/api-server/.env:** +```env +MONGODB_URI=mongodb://localhost:27017/free5gc +REDIS_URI=redis://localhost:6379 +API_PORT=8000 +GIN_MODE=debug +``` + +**apps/charging-engine/.env:** +```env +REDIS_URI=redis://127.0.0.1/ +SERVER_PORT=8080 +RUST_LOG=info +``` + +**apps/web-dashboard/.env.local:** +```env +NEXT_PUBLIC_API_URL=http://localhost:8000 +``` + +--- + +## Docker Deployment + +### Build Images + +```bash +# Build all Docker images +make docker-build + +# Or build individually +docker build -f deployments/docker/api-server.Dockerfile -t taas-api-server . +docker build -f deployments/docker/charging-engine.Dockerfile -t taas-charging-engine . +docker build -f deployments/docker/web-dashboard.Dockerfile -t taas-web-dashboard . +``` + +### Run with Docker Compose + +```bash +# Start all services +docker-compose up -d + +# View logs +docker-compose logs -f + +# Stop all services +docker-compose down + +# Stop and remove volumes +docker-compose down -v +``` + +### Service URLs + +After starting with docker-compose: + +- **API Server**: http://localhost:8000 +- **Charging Engine**: http://localhost:8080 +- **Web Dashboard**: http://localhost:3000 +- **MongoDB**: mongodb://localhost:27017 +- **Redis**: redis://localhost:6379 + +### Troubleshooting + +**MongoDB connection issues:** +```bash +# Check if MongoDB is running +docker-compose ps mongodb + +# View MongoDB logs +docker-compose logs mongodb + +# Connect to MongoDB shell +docker exec -it taas-mongodb mongosh +``` + +**Redis connection issues:** +```bash +# Test Redis connection +docker exec -it taas-redis redis-cli ping +``` + +--- + +## Kubernetes Deployment + +### Prerequisites + +- Kubernetes cluster (v1.28+) +- kubectl configured +- Persistent storage provisioner +- LoadBalancer service support (or Ingress controller) + +### Deploy to Kubernetes + +```bash +# 1. Create namespace +kubectl create namespace taas-platform + +# 2. Deploy all manifests +kubectl apply -f deployments/kubernetes/ + +# 3. Verify deployments +kubectl get pods -n taas-platform + +# Expected output: +# NAME READY STATUS RESTARTS AGE +# mongodb-xxx 1/1 Running 0 2m +# redis-xxx 1/1 Running 0 2m +# api-server-xxx 1/1 Running 0 1m +# charging-engine-xxx 1/1 Running 0 1m +``` + +### Access Services + +```bash +# Get API Server external IP +kubectl get svc api-server -n taas-platform + +# Port forward for local access +kubectl port-forward -n taas-platform svc/api-server 8000:8000 +kubectl port-forward -n taas-platform svc/charging-engine 8080:8080 +``` + +### Scaling + +```bash +# Scale API Server +kubectl scale deployment api-server -n taas-platform --replicas=5 + +# Scale Charging Engine +kubectl scale deployment charging-engine -n taas-platform --replicas=3 + +# Enable autoscaling +kubectl autoscale deployment api-server -n taas-platform \ + --min=3 --max=10 --cpu-percent=70 +``` + +### Monitoring + +```bash +# View logs +kubectl logs -f deployment/api-server -n taas-platform + +# Describe pod for issues +kubectl describe pod -n taas-platform + +# Get events +kubectl get events -n taas-platform --sort-by='.lastTimestamp' +``` + +### Update Deployment + +```bash +# Update image +kubectl set image deployment/api-server \ + api-server=taas-api-server:v2.0.0 \ + -n taas-platform + +# Check rollout status +kubectl rollout status deployment/api-server -n taas-platform + +# Rollback if needed +kubectl rollout undo deployment/api-server -n taas-platform +``` + +--- + +## Production Considerations + +### High Availability + +**MongoDB Replica Set:** +```yaml +# Use MongoDB Operator or StatefulSet +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: mongodb +spec: + replicas: 3 + serviceName: mongodb + # ... (see MongoDB Operator docs) +``` + +**Redis Cluster:** +```bash +# Deploy Redis in cluster mode +helm install redis bitnami/redis-cluster \ + --namespace taas-platform \ + --set cluster.nodes=6 \ + --set cluster.replicas=1 +``` + +### Security + +**Secrets Management:** +```bash +# Create Kubernetes secrets +kubectl create secret generic mongodb-secret \ + --from-literal=username=admin \ + --from-literal=password='STRONG_PASSWORD' \ + -n taas-platform + +kubectl create secret generic api-keys \ + --from-literal=jwt-secret='JWT_SECRET' \ + -n taas-platform +``` + +**Network Policies:** +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: api-server-policy + namespace: taas-platform +spec: + podSelector: + matchLabels: + app: api-server + policyTypes: + - Ingress + - Egress + ingress: + - from: + - podSelector: + matchLabels: + app: web-dashboard + ports: + - protocol: TCP + port: 8000 + egress: + - to: + - podSelector: + matchLabels: + app: mongodb + ports: + - protocol: TCP + port: 27017 +``` + +### Monitoring Stack + +**Prometheus & Grafana:** +```bash +# Install using Helm +helm repo add prometheus-community https://prometheus-community.github.io/helm-charts +helm install prometheus prometheus-community/kube-prometheus-stack \ + -n monitoring --create-namespace +``` + +**Application Metrics:** +- Add `/metrics` endpoint to each service +- Use Prometheus client libraries +- Create Grafana dashboards + +### Backup Strategy + +**MongoDB Backups:** +```bash +# Daily backups with mongodump +kubectl create cronjob mongodb-backup \ + --image=mongo:7.0 \ + --schedule="0 2 * * *" \ + -- mongodump --uri=$MONGODB_URI --out=/backup +``` + +**Redis Persistence:** +```bash +# Enable AOF in Redis config +kubectl edit configmap redis-config -n taas-platform +# Set: appendonly yes +``` + +### Resource Limits + +**Recommended limits per service:** + +| Service | CPU Request | CPU Limit | Memory Request | Memory Limit | +|---------|-------------|-----------|----------------|--------------| +| API Server | 100m | 500m | 128Mi | 512Mi | +| Charging Engine | 100m | 1000m | 128Mi | 512Mi | +| Packet Gateway | 250m | 2000m | 256Mi | 1Gi | +| MongoDB | 250m | 1000m | 512Mi | 2Gi | +| Redis | 100m | 500m | 256Mi | 2Gi | + +### Load Testing + +```bash +# Install k6 +brew install k6 # macOS +# or: sudo apt-get install k6 + +# Run load test +k6 run scripts/loadtest.js +``` + +**Sample load test script:** +```javascript +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +export const options = { + stages: [ + { duration: '2m', target: 100 }, // Ramp up + { duration: '5m', target: 100 }, // Stay at 100 users + { duration: '2m', target: 200 }, // Ramp to 200 + { duration: '5m', target: 200 }, // Stay at 200 + { duration: '2m', target: 0 }, // Ramp down + ], +}; + +export default function () { + const res = http.get('http://api-server:8000/health'); + check(res, { 'status is 200': (r) => r.status === 200 }); + sleep(1); +} +``` + +### Disaster Recovery + +**Backup Locations:** +- MongoDB backups: S3 bucket (cross-region replication) +- Redis RDB snapshots: S3 bucket +- Configuration: Git repository +- Secrets: HashiCorp Vault or AWS Secrets Manager + +**Recovery Time Objective (RTO):** <1 hour +**Recovery Point Objective (RPO):** <15 minutes + +**Recovery Procedure:** +1. Spin up new Kubernetes cluster +2. Restore MongoDB from latest backup +3. Restore Redis from snapshot +4. Deploy all services from Git +5. Update DNS to point to new cluster +6. Verify functionality + +### CI/CD Pipeline + +**GitHub Actions example:** +```yaml +name: Deploy + +on: + push: + branches: [main] + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Build Docker images + run: make docker-build + + - name: Push to registry + run: | + docker tag taas-api-server registry.example.com/taas-api-server:${{ github.sha }} + docker push registry.example.com/taas-api-server:${{ github.sha }} + + - name: Deploy to Kubernetes + run: | + kubectl set image deployment/api-server \ + api-server=registry.example.com/taas-api-server:${{ github.sha }} \ + -n taas-platform +``` + +### Cost Optimization + +**Cloud Provider Recommendations:** +- Use spot/preemptible instances for non-critical services +- Enable autoscaling to match load +- Use reserved instances for predictable workloads +- Store backups in cheaper storage classes (S3 Glacier) + +**Estimated Monthly Costs (AWS):** +- 3x t3.medium (API Server): $90 +- 2x t3.medium (Charging Engine): $60 +- 1x t3.large (MongoDB): $75 +- 1x t3.small (Redis): $19 +- Load Balancer: $25 +- Data transfer: Variable +- **Total**: ~$270/month (base infrastructure) + +--- + +## Support + +For deployment issues: +- Check logs: `kubectl logs -f ` +- Review events: `kubectl get events` +- Join community: [Discussions](https://github.com/taas-platform/discussions) +- Email: devops@taas-platform.com diff --git a/go.work b/go.work new file mode 100644 index 0000000..e7d6b58 --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.26 + +use ( + ./apps/api-server + ./apps/carrier-connector +) diff --git a/package.json b/package.json new file mode 100644 index 0000000..52e6e8f --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "telecom-platform", + "version": "1.0.0", + "description": "Sovereign Telecom-as-a-Service Platform", + "private": true, + "scripts": { + "dev": "pnpm --filter web-dashboard dev", + "build": "pnpm -r run build", + "lint": "pnpm -r run lint", + "test": "pnpm -r run test", + "clean": "pnpm -r exec rm -rf .next dist node_modules" + }, + "keywords": [ + "telecom", + "5g", + "esim", + "taas", + "cellular", + "connectivity" + ], + "author": "TaaS Team", + "license": "MIT", + "engines": { + "node": ">=22.0.0", + "pnpm": ">=9.0.0" + }, + "packageManager": "pnpm@9.0.0" +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..3601ff6 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - 'apps/web-dashboard' + - 'libs/shared-ts-sdk' diff --git a/scripts/dev-setup.sh b/scripts/dev-setup.sh new file mode 100755 index 0000000..c576a90 --- /dev/null +++ b/scripts/dev-setup.sh @@ -0,0 +1,144 @@ +#!/bin/bash +set -e + +echo "=========================================" +echo "TaaS Platform - Development Setup" +echo "=========================================" +echo "" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to check command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Check Go 1.26 +echo -n "Checking Go 1.26... " +if command_exists go; then + GO_VERSION=$(go version | awk '{print $3}' | sed 's/go//') + if [[ "$GO_VERSION" == 1.26* ]]; then + echo -e "${GREEN}✓${NC} Found $(go version)" + else + echo -e "${YELLOW}⚠${NC} Found Go $GO_VERSION (1.26 recommended)" + fi +else + echo -e "${RED}✗${NC} Not found" + echo "Please install Go 1.26: https://go.dev/dl/" +fi + +# Check Rust 1.94 +echo -n "Checking Rust 1.94... " +if command_exists rustc; then + RUST_VERSION=$(rustc --version | awk '{print $2}') + if [[ "$RUST_VERSION" == 1.94* ]]; then + echo -e "${GREEN}✓${NC} Found $(rustc --version)" + else + echo -e "${YELLOW}⚠${NC} Found Rust $RUST_VERSION (1.94 recommended)" + fi +else + echo -e "${RED}✗${NC} Not found" + echo "Please install Rust: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh" +fi + +# Check Node.js 22 +echo -n "Checking Node.js 22... " +if command_exists node; then + NODE_VERSION=$(node --version | sed 's/v//') + if [[ "$NODE_VERSION" == 22* ]]; then + echo -e "${GREEN}✓${NC} Found $(node --version)" + else + echo -e "${YELLOW}⚠${NC} Found Node.js $NODE_VERSION (22 recommended)" + fi +else + echo -e "${RED}✗${NC} Not found" + echo "Please install Node.js 22: https://nodejs.org/" +fi + +# Check pnpm +echo -n "Checking pnpm... " +if command_exists pnpm; then + echo -e "${GREEN}✓${NC} Found $(pnpm --version)" +else + echo -e "${RED}✗${NC} Not found" + echo "Installing pnpm..." + npm install -g pnpm +fi + +# Check MongoDB +echo -n "Checking MongoDB... " +if command_exists mongosh; then + echo -e "${GREEN}✓${NC} Found" +else + echo -e "${YELLOW}⚠${NC} MongoDB shell not found" + echo "Please install MongoDB: https://www.mongodb.com/docs/manual/installation/" +fi + +# Check Redis +echo -n "Checking Redis... " +if command_exists redis-cli; then + echo -e "${GREEN}✓${NC} Found" +else + echo -e "${YELLOW}⚠${NC} Redis not found" + echo "Install Redis: sudo apt-get install redis-server" +fi + +# Check Docker +echo -n "Checking Docker... " +if command_exists docker; then + echo -e "${GREEN}✓${NC} Found $(docker --version | cut -d ' ' -f3)" +else + echo -e "${YELLOW}⚠${NC} Docker not found (optional)" +fi + +echo "" +echo "=========================================" +echo "Installing Dependencies" +echo "=========================================" +echo "" + +# Install Go dependencies +echo "Installing Go dependencies..." +cd apps/api-server && go mod download && cd ../.. +cd apps/carrier-connector && go mod download && cd ../.. + +# Install Rust dependencies +echo "Installing Rust dependencies..." +cargo fetch + +# Install Node.js dependencies +echo "Installing Node.js dependencies..." +pnpm install + +echo "" +echo "=========================================" +echo "Creating Environment Files" +echo "=========================================" +echo "" + +# Create .env files from examples +for env_example in $(find . -name ".env.example"); do + env_file="${env_example%.example}" + if [ ! -f "$env_file" ]; then + cp "$env_example" "$env_file" + echo "Created $env_file" + else + echo "Skipped $env_file (already exists)" + fi +done + +echo "" +echo "=========================================" +echo "Setup Complete!" +echo "=========================================" +echo "" +echo "Next steps:" +echo "1. Set up databases: make db-setup" +echo "2. Build all components: make all" +echo "3. Start development: make dev" +echo "" +echo "For help: make help" diff --git a/scripts/setup-mongodb.sh b/scripts/setup-mongodb.sh new file mode 100755 index 0000000..dd230d3 --- /dev/null +++ b/scripts/setup-mongodb.sh @@ -0,0 +1,58 @@ +#!/bin/bash +set -e + +echo "Setting up MongoDB for TaaS Platform..." + +# Check if MongoDB is installed +if ! command -v mongosh &> /dev/null; then + echo "MongoDB shell (mongosh) not found. Please install MongoDB first." + echo "Visit: https://www.mongodb.com/docs/manual/installation/" + exit 1 +fi + +# MongoDB connection details +MONGO_HOST=${MONGO_HOST:-"localhost"} +MONGO_PORT=${MONGO_PORT:-"27017"} +MONGO_USER=${MONGO_USER:-"free5gc"} +MONGO_PASS=${MONGO_PASS:-"free5gc_password"} +MONGO_DB="free5gc" + +echo "Creating MongoDB database and user..." + +# Create database and user +mongosh --host $MONGO_HOST --port $MONGO_PORT < /dev/null; then + echo "Redis CLI not found. Please install Redis first." + echo "Ubuntu/Debian: sudo apt-get install redis-server" + echo "macOS: brew install redis" + exit 1 +fi + +# Redis connection details +REDIS_HOST=${REDIS_HOST:-"localhost"} +REDIS_PORT=${REDIS_PORT:-"6379"} + +echo "Testing Redis connection..." + +# Test connection +if redis-cli -h $REDIS_HOST -p $REDIS_PORT ping > /dev/null 2>&1; then + echo "✓ Redis is running and accessible" +else + echo "✗ Cannot connect to Redis at $REDIS_HOST:$REDIS_PORT" + echo "Please start Redis: sudo systemctl start redis-server" + exit 1 +fi + +# Configure Redis for TaaS +echo "Configuring Redis..." + +redis-cli -h $REDIS_HOST -p $REDIS_PORT <