feat: geo-restriction via Cloudflare WAF custom rules
Add country-based access control managed through the Settings page.
Global allow-list with per-app overrides, searchable country selector,
automatic sync to Cloudflare WAF on settings change / deploy / remove,
plus periodic 6-hour verification.
New package: internal/cloudflare/ (client, zone, waf, countries, geosync)
New API: /api/geo/* (6 endpoints) + /api/stacks/{name}/geo/override
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ import (
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/api"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/assets"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
||||
cf "gitea.dooplex.hu/admin/felhom-controller/internal/cloudflare"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/crypto"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/metrics"
|
||||
@@ -604,6 +605,47 @@ func main() {
|
||||
if assetsSyncer != nil {
|
||||
apiRouter.SetAssetsSyncer(assetsSyncer)
|
||||
}
|
||||
|
||||
// --- Initialize Cloudflare geo-restriction ---
|
||||
var geoSync *cf.GeoSyncManager
|
||||
if cfg.Infrastructure.CFAPIToken != "" {
|
||||
cfClient := cf.New(cfg.Infrastructure.CFAPIToken, logger, cfg.Logging.Level == "debug")
|
||||
geoStacks := &geoStackAdapter{mgr: stackMgr, domain: cfg.Customer.Domain}
|
||||
geoSync = cf.NewGeoSyncManager(cfClient, sett, cfg.Customer.Domain, geoStacks, logger)
|
||||
apiRouter.SetGeoSync(geoSync)
|
||||
|
||||
// Re-sync geo rules when apps are deployed/removed
|
||||
apiRouter.OnGeoRelevantChange = func() {
|
||||
geo := sett.GetGeoRestriction()
|
||||
if geo != nil && geo.Enabled {
|
||||
if err := geoSync.Sync(context.Background()); err != nil {
|
||||
logger.Printf("[WARN] Geo sync after app change failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Periodic verification every 6 hours
|
||||
sched.Every("geo-verify", 6*time.Hour, func(ctx context.Context) error {
|
||||
geo := sett.GetGeoRestriction()
|
||||
if geo == nil || !geo.Enabled {
|
||||
return nil
|
||||
}
|
||||
return geoSync.Sync(ctx)
|
||||
})
|
||||
|
||||
// Initial sync (delayed, non-blocking)
|
||||
go func() {
|
||||
time.Sleep(15 * time.Second)
|
||||
if geo := sett.GetGeoRestriction(); geo != nil && geo.Enabled {
|
||||
if err := geoSync.Sync(context.Background()); err != nil {
|
||||
logger.Printf("[WARN] Initial geo sync failed: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
logger.Printf("[INFO] Geo-restriction support enabled (CF API token configured)")
|
||||
}
|
||||
|
||||
// --- Initialize web server ---
|
||||
webServer := web.NewServer(cfg, stackMgr, cpuCollector, backupMgr, crossDriveRunner, sched, sett, alertMgr, notifier, updater, logger, Version)
|
||||
webServer.SetEncryptionKey(encKey)
|
||||
@@ -871,6 +913,32 @@ func (a *stackAdapter) GetStackHDDPath(name string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// geoStackAdapter implements cloudflare.StackLister for geo-restriction sync.
|
||||
type geoStackAdapter struct {
|
||||
mgr *stacks.Manager
|
||||
domain string
|
||||
}
|
||||
|
||||
func (a *geoStackAdapter) GetDeployedHostnames() map[string]string {
|
||||
result := make(map[string]string)
|
||||
for _, stack := range a.mgr.GetStacks() {
|
||||
if !stack.Deployed {
|
||||
continue
|
||||
}
|
||||
subdomain := stack.Meta.Subdomain
|
||||
// Check for custom subdomain in app.yaml
|
||||
if appCfg := a.mgr.LoadAppConfigByName(stack.Name); appCfg != nil {
|
||||
if sd, ok := appCfg.Env["SUBDOMAIN"]; ok && sd != "" {
|
||||
subdomain = sd
|
||||
}
|
||||
}
|
||||
if subdomain != "" {
|
||||
result[stack.Name] = subdomain + "." + a.domain
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// watchdogStackAdapter implements monitor.WatchdogStackProvider using stacks.Manager.
|
||||
type watchdogStackAdapter struct {
|
||||
mgr *stacks.Manager
|
||||
|
||||
Reference in New Issue
Block a user