security updates
This commit is contained in:
@@ -6,6 +6,9 @@ services:
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
user: "1000:1000"
|
||||
env_file: .env
|
||||
networks:
|
||||
- edge
|
||||
|
||||
@@ -24,6 +24,10 @@ FRPS_PASS = os.environ["FRPS_DASHBOARD_PASS"]
|
||||
SUPABASE_URL = os.environ.get("SUPABASE_URL", "http://supabase-kong:8000")
|
||||
SERVICE_ROLE = os.environ["SUPABASE_SERVICE_ROLE_KEY"]
|
||||
REDIS_URL = os.environ["REDIS_URL"]
|
||||
# TTL for the edge active-state cache key (tunnel:active:<sub>) read by the
|
||||
# Caddy forward_auth gate. Short so a deactivation propagates fast and a stale
|
||||
# "1" self-heals within one poll cycle; refreshed every poll while active.
|
||||
ACTIVE_TTL = int(os.environ.get("ACTIVE_STATE_TTL_SECONDS", "45"))
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
@@ -98,6 +102,13 @@ def enforce_quota(client: httpx.Client, subdomain: str, row: dict[str, Any], new
|
||||
except Exception as e:
|
||||
log.warning("redis auth-cache invalidate failed for %s: %s", subdomain, e)
|
||||
|
||||
# Flip the edge active-state key so the Caddy forward_auth gate denies the
|
||||
# next request (within ~1 request) even on a still-connected tunnel.
|
||||
try:
|
||||
rds.set(f"tunnel:active:{subdomain}", "0", ex=ACTIVE_TTL)
|
||||
except Exception as e:
|
||||
log.warning("redis active-state deactivate failed for %s: %s", subdomain, e)
|
||||
|
||||
log.warning(
|
||||
"QUOTA EXCEEDED: subdomain=%s used=%d quota=%d -> deactivated",
|
||||
subdomain, new_used, quota,
|
||||
@@ -145,16 +156,49 @@ def record_delta(client: httpx.Client, subdomain: str, delta: int) -> None:
|
||||
|
||||
enforce_quota(client, subdomain, row, new_used)
|
||||
|
||||
# Keep the edge active-state key fresh so the forward_auth gate has a hot,
|
||||
# authoritative value (covers reactivation and TTL expiry of an active key).
|
||||
quota = int(row.get("quota_bytes") or 0)
|
||||
active_now = bool(row.get("is_active")) and (quota <= 0 or new_used < quota)
|
||||
try:
|
||||
rds.set(f"tunnel:active:{subdomain}", "1" if active_now else "0", ex=ACTIVE_TTL)
|
||||
except Exception as e:
|
||||
log.warning("redis active-state refresh failed for %s: %s", subdomain, e)
|
||||
|
||||
|
||||
_schema_checked = False
|
||||
|
||||
|
||||
def assert_api_schema(proxies: list[dict[str, Any]]) -> None:
|
||||
"""Fail loud if the frps dashboard API stops returning the expected
|
||||
camelCase traffic fields (e.g. after an frps upgrade), so the quota
|
||||
accounting bug cannot silently return."""
|
||||
global _schema_checked
|
||||
if _schema_checked or not proxies:
|
||||
return
|
||||
sample = proxies[0]
|
||||
if "todayTrafficIn" not in sample:
|
||||
log.error(
|
||||
"frps API schema mismatch: 'todayTrafficIn' missing from "
|
||||
"proxy keys=%s; quota accounting is DISABLED until the field "
|
||||
"names are fixed",
|
||||
sorted(sample.keys()),
|
||||
)
|
||||
else:
|
||||
log.info("frps API schema OK: todayTrafficIn present")
|
||||
_schema_checked = True
|
||||
|
||||
|
||||
def poll_once(client: httpx.Client) -> None:
|
||||
proxies = fetch_proxies(client)
|
||||
log.info("poll: %d proxies", len(proxies))
|
||||
assert_api_schema(proxies)
|
||||
for p in proxies:
|
||||
name = p.get("name") or ""
|
||||
if not name:
|
||||
continue
|
||||
traffic_in = int(p.get("today_traffic_in") or 0)
|
||||
traffic_out = int(p.get("today_traffic_out") or 0)
|
||||
traffic_in = int(p.get("todayTrafficIn") or 0)
|
||||
traffic_out = int(p.get("todayTrafficOut") or 0)
|
||||
total = traffic_in + traffic_out
|
||||
prev = previous_total(name)
|
||||
delta = total - prev
|
||||
|
||||
Reference in New Issue
Block a user