Skip to main content

Reverse Tunnels Infrastructure

Overview

Infrastructure components for enabling public URL access to device web services via AWS IoT Secure Tunneling. Public URL Format: https://device-{dsn}.fleet.roboticks.io

CDK Stack Components

All components are integrated into the main RoboticksStack in lib/roboticks-stack.ts (lines 1165-1430).

1. DynamoDB Cache Table

Resource: ReverseTunnelCache Purpose: Fast lookup of tunnel configurations for proxy Lambda Schema:
{
  partitionKey: 'public_url',  // device-{dsn}.fleet.roboticks.io
  sortKey: none,
  attributes: {
    tunnel_id: string,
    source_access_token: string,
    destination_access_token: string,
    device_id: number,
    local_port: number,
    protocol: string,
    is_enabled: boolean,
    expires_at: string (ISO 8601),
    ttl: number (Unix timestamp)
  }
}
GSI: tunnel-id-index on tunnel_id for reverse lookups TTL: Automatic deletion when tunnels expire Billing: PAY_PER_REQUEST (serverless)

2. Reverse Tunnel Proxy Lambda

Resource: ReverseTunnelProxy Runtime: Node.js 20.x Purpose: Route HTTP/WebSocket traffic to AWS IoT Secure Tunnels Code Location: lambda/reverse-tunnel-proxy/ Handler: index.handler Timeout: 30 seconds Memory: 512 MB VPC: Connected to private subnets with database access Environment Variables:
  • TUNNEL_CACHE_TABLE - DynamoDB table name
  • DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD - PostgreSQL
  • AWS_IOT_REGION - Region for IoT Secure Tunneling
IAM Permissions:
  • dynamodb:GetItem, dynamodb:PutItem on tunnel cache table
  • secretsmanager:GetSecretValue for database credentials
  • iotsecuretunneling:DescribeTunnel, iotsecuretunneling:OpenTunnel
Current Implementation:
  • ✅ DSN extraction from subdomain
  • ✅ DynamoDB cache lookup with PostgreSQL fallback
  • ✅ AWS IoT tunnel status verification
  • ⏳ WebSocket proxy implementation (TODO)

3. Tunnel Cache Warmer Lambda

Resource: TunnelCacheWarmer Runtime: Python 3.11 Purpose: Sync tunnel configs from PostgreSQL to DynamoDB every 5 minutes Code Location: lambda/tunnel-cache-warmer/ Handler: index.lambda_handler Timeout: 2 minutes Memory: 256 MB (default) VPC: Connected to private subnets with database access Environment Variables:
  • TUNNEL_CACHE_TABLE - DynamoDB table name
  • DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD - PostgreSQL
IAM Permissions:
  • dynamodb:PutItem on tunnel cache table
  • secretsmanager:GetSecretValue for database credentials
Schedule: Every 5 minutes via EventBridge Query:
SELECT * FROM reverse_tunnels
WHERE is_enabled = true
  AND tunnel_id IS NOT NULL
  AND expires_at > NOW()

4. EventBridge Schedule

Resource: TunnelCacheWarmerRule Schedule: Rate(5 minutes) Target: TunnelCacheWarmer Lambda Purpose: Keep DynamoDB cache in sync with PostgreSQL

5. Backend IAM Permissions

Added to: backendTaskDefinition.taskRole Permissions:
{
  actions: [
    'iotsecuretunneling:OpenTunnel',
    'iotsecuretunneling:CloseTunnel',
    'iotsecuretunneling:DescribeTunnel',
    'iotsecuretunneling:ListTunnels',
  ],
  resources: ['*']
}
Purpose: Backend API can create/manage reverse tunnels

6. Backend Environment Variables

Added to: backendContainer Variable: TUNNEL_CACHE_TABLE = roboticks-reverse-tunnel-cache Purpose: Backend can read/write tunnel cache for real-time updates

7. API Gateway HTTP API

Resource: ReverseTunnelApi Purpose: HTTP endpoint for Lambda proxy integration Type: HTTP API (supports HTTP/1.1, HTTP/2, and WebSocket) Routes:
  • /{proxy+} - Catch-all route for all paths
  • / - Root route
Integration: Lambda proxy integration with ReverseTunnelProxy CORS: Configured for wildcard origins URL Format: https://{api-id}.execute-api.{region}.amazonaws.com

8. ACM Certificate

Resource: FleetCertificate Domain: *.fleet.roboticks.io (wildcard) Validation: DNS validation via Route53 Purpose: TLS certificate for CloudFront distribution Region: Same as stack (us-east-1 required for CloudFront)

9. CloudFront Distribution

Resource: TunnelDistribution Purpose: TLS termination, caching, and DDoS protection Origin: API Gateway HTTP API Custom Domains: *.fleet.roboticks.io Certificate: FleetCertificate (ACM) Behavior:
  • Viewer Protocol: Redirect HTTP to HTTPS
  • Allowed Methods: ALL (GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD)
  • Cache Policy: CACHING_DISABLED (proxy traffic, no caching)
  • Origin Request Policy: ALL_VIEWER (forward all headers/cookies)
Logging: Enabled to S3 (cloudfront-tunnel-logs/ prefix) Price Class: PRICE_CLASS_100 (North America + Europe only)

10. Route53 DNS Records

Resources: FleetWildcardARecord, FleetWildcardAAAARecord Domain: *.fleet.roboticks.io Type: A (IPv4) and AAAA (IPv6) alias records Target: CloudFront distribution Purpose: Route all device subdomains to CloudFront Examples:
  • device-abc123.fleet.roboticks.io → CloudFront → API Gateway → Lambda
  • device-rpi001.fleet.roboticks.io → CloudFront → API Gateway → Lambda

Data Flow

Tunnel Creation (Backend → AWS)

  1. User creates reverse tunnel via frontend UI
  2. Backend API calls IoTSecureTunnelService.create_reverse_tunnel()
  3. Service creates AWS IoT Secure Tunnel
  4. Service stores tunnel_id + tokens in PostgreSQL reverse_tunnels table
  5. Backend writes to DynamoDB cache immediately
  6. Cache warmer syncs to DynamoDB every 5 minutes (redundancy)

Traffic Proxying (Browser → Device)

  1. Browser navigates to https://device-abc123.fleet.roboticks.io
  2. Route53 DNS resolves *.fleet.roboticks.io to CloudFront distribution
  3. CloudFront terminates TLS using ACM certificate
  4. CloudFront forwards request to API Gateway HTTP API origin
  5. API Gateway invokes ReverseTunnelProxy Lambda
  6. Lambda extracts DSN from subdomain (device-abc123)
  7. Lambda queries DynamoDB cache for tunnel configuration
  8. Lambda falls back to PostgreSQL if cache miss
  9. Lambda verifies tunnel is OPEN in AWS IoT Secure Tunneling
  10. Lambda forwards traffic to IoT Secure Tunnel using source_access_token (TODO)
  11. Device local proxy agent receives traffic via destination_access_token
  12. Device forwards to local service (localhost:8080)

Cache Synchronization (PostgreSQL → DynamoDB)

  1. EventBridge triggers TunnelCacheWarmer every 5 minutes
  2. Lambda queries PostgreSQL for active tunnels
  3. Lambda writes each tunnel to DynamoDB with TTL
  4. DynamoDB auto-deletes expired entries based on TTL

Stack Outputs

CDK Outputs (added to stack):
ReverseTunnelProxyArn: arn:aws:lambda:{region}:{account}:function/RoboticksStack-ReverseTunnelProxy
TunnelCacheTableName: roboticks-reverse-tunnel-cache
TunnelApiUrl: https://{api-id}.execute-api.{region}.amazonaws.com
TunnelDistributionDomain: d123abc.cloudfront.net
FleetDomainSetup: *.fleet.roboticks.io -> CloudFront (auto-configured)
FleetCertificateArn: arn:aws:acm:{region}:{account}:certificate/{id}
Access Device Tunnel: Once deployed, device web services are accessible at:
https://device-{dsn}.fleet.roboticks.io
Example:
# Device with DSN "rpi001"
curl https://device-rpi001.fleet.roboticks.io

# Returns tunnel status (proxy logic pending)
{
  "message": "Reverse tunnel is active",
  "dsn": "rpi001",
  "public_url": "device-rpi001.fleet.roboticks.io",
  "tunnel_id": "aws-tunnel-id-123",
  "tunnel_status": "OPEN",
  "local_service": "http://localhost:8080"
}

Deployment

cd infrastructure

# Install dependencies
npm install

# Synthesize CloudFormation
cdk synth

# Deploy stack
cdk deploy

# View outputs
aws cloudformation describe-stacks \
  --stack-name RoboticksStack \
  --query "Stacks[0].Outputs"

Testing

Test Lambda Functions Locally

Reverse Tunnel Proxy:
cd lambda/reverse-tunnel-proxy
npm install
node -e "
const handler = require('./index').handler;
handler({
  headers: { host: 'device-abc123.fleet.roboticks.io' }
}, {})
  .then(r => console.log(r))
  .catch(e => console.error(e));
"
Cache Warmer:
cd lambda/tunnel-cache-warmer
pip install -r requirements.txt
python -c "
from index import lambda_handler
result = lambda_handler({}, None)
print(result)
"

Invoke Lambda via AWS CLI

# Invoke proxy Lambda
aws lambda invoke \
  --function-name RoboticksStack-ReverseTunnelProxy \
  --payload '{"headers":{"host":"device-abc123.fleet.roboticks.io"}}' \
  response.json

# Invoke cache warmer Lambda
aws lambda invoke \
  --function-name RoboticksStack-TunnelCacheWarmer \
  response.json

Query DynamoDB Cache

# Get tunnel by public URL
aws dynamodb get-item \
  --table-name roboticks-reverse-tunnel-cache \
  --key '{"public_url": {"S": "device-abc123.fleet.roboticks.io"}}'

# Scan all tunnels
aws dynamodb scan \
  --table-name roboticks-reverse-tunnel-cache

Monitoring

CloudWatch Logs

Proxy Lambda:
aws logs tail /aws/lambda/RoboticksStack-ReverseTunnelProxy --follow
Cache Warmer:
aws logs tail /aws/lambda/RoboticksStack-TunnelCacheWarmer --follow

CloudWatch Metrics

Key Metrics:
  • ReverseTunnelProxy.Invocations - Proxy request count
  • ReverseTunnelProxy.Errors - Proxy error rate
  • ReverseTunnelProxy.Duration - Proxy latency
  • TunnelCacheWarmer.Invocations - Cache sync frequency
  • DynamoDB.ConsumedReadCapacityUnits - Cache read load
  • DynamoDB.ConsumedWriteCapacityUnits - Cache write load
// Proxy error rate alarm
new cloudwatch.Alarm(this, 'ProxyErrorAlarm', {
  metric: reverseTunnelProxy.metricErrors(),
  threshold: 10,
  evaluationPeriods: 1,
  alarmDescription: 'Reverse tunnel proxy error rate too high',
});

// Cache warmer failure alarm
new cloudwatch.Alarm(this, 'CacheWarmerFailureAlarm', {
  metric: cacheWarmerLambda.metricErrors(),
  threshold: 3,
  evaluationPeriods: 1,
  alarmDescription: 'Cache warmer failing repeatedly',
});

Cost Estimation

DynamoDB

  • Capacity: PAY_PER_REQUEST (serverless)
  • Storage: First 25 GB free
  • Reads: $0.25 per million requests
  • Writes: $1.25 per million requests
Example: 100 devices, 1000 requests/device/day = 3M reads/month = $0.75/month

Lambda

Proxy Lambda:
  • Requests: 100 devices × 1000 req/day × 30 days = 3M requests/month
  • Duration: 500ms average
  • Cost: ~$10/month (request charges + compute time)
Cache Warmer:
  • Requests: 12 invocations/hour × 24 hours × 30 days = 8,640 invocations/month
  • Duration: 2-5 seconds
  • Cost: ~$0.10/month (negligible)
Total Lambda Cost: ~$10-15/month for 100 active devices

Total Estimated Cost

100 Devices: ~1520/month1000Devices: 15-20/month **1000 Devices**: ~100-150/month

Next Steps

Infrastructure: ✅ Complete Remaining Implementation:
  1. Lambda Proxy Logic (~12-16 hours):
    • Implement WebSocket-to-WebSocket bidirectional forwarding
    • Implement HTTP-to-IoT-Tunnel request proxying
    • Add AWS IoT Secure Tunneling Local Proxy protocol support
    • Handle connection lifecycle, errors, and retries
  2. Device SDK (~4 hours):
    • Add reverse tunnel manager to roboticks-sdk
    • Fetch tunnel config on device startup
    • Start AWS IoT local proxy agents
  3. Monitoring (~2 hours):
    • Add CloudWatch alarms for proxy errors
    • Add CloudWatch alarms for cache warmer failures
    • Create dashboard for tunnel metrics
  4. Testing (~4 hours):
    • End-to-end testing with real devices
    • Load testing for concurrent tunnels
    • Failover testing (expired tunnels, rotation)
Optional Enhancements:
  • Cloudflare proxy (currently optional, CloudFront provides DDoS protection)
  • Custom CloudWatch metrics for tunnel usage
  • X-Ray tracing for Lambda proxy
  • API Gateway throttling limits per device

References