Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,34 @@
If you are running the backend via Docker, the exposed ports are determined by the compose configuration. To use a different port in a Docker environment, you must manually update the docker-compose.yml file to adjust the container’s port mapping.
Also, if you change `CCSYNC_PORT`, remember to update `CONTAINER_ORIGIN` accordingly.

### Rate Limiting and Trusted Proxies

The backend includes rate limiting that uses the client's IP address. When running behind a reverse proxy (like nginx), you need to configure trusted proxies so the backend correctly identifies client IPs from proxy headers.

**Automatic Trust:**
- Loopback addresses (127.0.0.1, ::1) are always trusted
- In production (`ENV=production`), Docker bridge networks (172.16.0.0/12) are trusted

**Manual Configuration:**

Use `TRUSTED_PROXIES` to specify additional trusted proxy IPs or CIDR ranges:

```bash
# Single IP
TRUSTED_PROXIES="10.0.0.1"

# Multiple IPs (comma-separated)
TRUSTED_PROXIES="10.0.0.1,10.0.0.2"

# CIDR notation
TRUSTED_PROXIES="10.0.0.0/8,192.168.0.0/16"
```

**When to use:**
- **Local development**: Not needed (loopback is trusted by default)
- **Production with nginx on same server**: Not needed (loopback is trusted)
- **Production with external load balancer**: Set to your load balancer's IP/range

- Run the application:

```bash
Expand Down
89 changes: 77 additions & 12 deletions backend/middleware/ratelimit.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package middleware

import (
"fmt"
"net"
"net/http"
"os"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -103,23 +105,86 @@ func RateLimitMiddleware(limiter *RateLimiter) func(http.Handler) http.Handler {
}
}

func getRealIP(r *http.Request) string {
ip := r.Header.Get("X-Real-IP")
if ip != "" {
return ip
// isTrustedProxy checks if the request is from a trusted proxy.
// In production, we trust localhost connections (nginx running on same server).
// The TRUSTED_PROXIES env var can specify additional trusted proxy IPs (comma-separated).
func isTrustedProxy(remoteAddr string) bool {
// Extract IP from remoteAddr (format: "ip:port" or just "ip")
host, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
host = remoteAddr
}

// Parse the IP
ip := net.ParseIP(host)
if ip == nil {
return false
}

// In production, trust loopback (nginx on same server)
if ip.IsLoopback() {
return true
}

ip = r.Header.Get("X-Forwarded-For")
if ip != "" {
ips := strings.Split(ip, ",")
if len(ips) > 0 {
return strings.TrimSpace(ips[0])
// Check against TRUSTED_PROXIES env var
trustedProxies := os.Getenv("TRUSTED_PROXIES")
if trustedProxies != "" {
for _, trusted := range strings.Split(trustedProxies, ",") {
trusted = strings.TrimSpace(trusted)
// Support CIDR notation
if strings.Contains(trusted, "/") {
_, network, err := net.ParseCIDR(trusted)
if err == nil && network.Contains(ip) {
return true
}
} else {
trustedIP := net.ParseIP(trusted)
if trustedIP != nil && trustedIP.Equal(ip) {
return true
}
}
}
}

// In Docker environments, common bridge networks
// 172.17.0.0/16 is the default Docker bridge network
// 10.0.0.0/8 is commonly used for internal networks
if os.Getenv("ENV") == "production" {
// Trust Docker internal networks in production
dockerBridge := &net.IPNet{
IP: net.ParseIP("172.16.0.0"),
Mask: net.CIDRMask(12, 32),
}
if dockerBridge.Contains(ip) {
return true
}
}

return false
}

func getRealIP(r *http.Request) string {
// Only trust proxy headers if request is from a trusted proxy
if isTrustedProxy(r.RemoteAddr) {
// X-Real-IP is set by nginx
if ip := r.Header.Get("X-Real-IP"); ip != "" {
return ip
}

// X-Forwarded-For contains a list of IPs, take the first (original client)
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
ips := strings.Split(xff, ",")
if len(ips) > 0 {
return strings.TrimSpace(ips[0])
}
}
}

ip = r.RemoteAddr
if idx := strings.Index(ip, ":"); idx != -1 {
ip = ip[:idx]
// For direct connections or untrusted proxies, use RemoteAddr
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
// RemoteAddr might not have a port
return r.RemoteAddr
}

return ip
Expand Down
6 changes: 6 additions & 0 deletions production/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ REDIRECT_URL_DEV="https://your-domain.com/auth/callback"
SESSION_KEY="$(openssl rand -hex 32)"
FRONTEND_ORIGIN_DEV="https://your-domain.com"
CONTAINER_ORIGIN="http://syncserver:8080/"
ENV="production"

# Rate limiting: Trusted proxies for correct client IP detection
# Not needed if nginx runs on the same server (loopback is trusted by default)
# Only set this if using an external load balancer:
# TRUSTED_PROXIES="10.0.0.0/8,192.168.0.0/16"
```

Create `.frontend.env` (see `example.frontend.env`):
Expand Down
Loading