refactor: Enhance configuration and proxy middleware with logging and performance improvements
- Add configurable logging for config reloading in `init_config.go` - Implement in-memory cache for blacklisted IPs to improve lookup performance - Refactor proxy middleware to centralize blacklist and logging logic - Add methods for dynamically managing blacklist entries (add/remove IPs and ranges) - Improve logging of blocked IP requests with rate-limited summaries
- commit
- 5a8e00652ccf24b780181e2fe6df08c35e7a02d7
- author
- bamen · · 2025-03-09 08:44 UTC
- parent
- 411b756
- signature
- unsigned
3 files · +380 -142
M internal/common/init_config.go +94 -31
| @@ -80,6 +80,9 @@ var ( | |||
| 80 | 80 | gracePeriod = 30 // seconds | |
| 81 | 81 | ) | |
| 82 | 82 | ||
| 83 | + | // Track whether config logs have been printed already | |
| 84 | + | var configLogsPrinted bool = false | |
| 85 | + | ||
| 83 | 86 | func getConfigDir() (string, error) { | |
| 84 | 87 | var configDir string | |
| 85 | 88 | ||
| @@ -139,116 +142,162 @@ func readAndUnmarshalConfig(fs fs.FS, filePath string, config *Config) error { | |||
| 139 | 142 | } | |
| 140 | 143 | ||
| 141 | 144 | // loadConfigFromEnv loads configuration from environment variables | |
| 142 | - | func loadConfigFromEnv(config *Config) { | |
| 145 | + | func loadConfigFromEnv(config *Config, printLogs bool) { | |
| 143 | 146 | // General Configuration | |
| 144 | 147 | if val := os.Getenv("GORDON_STORAGE_DIR"); val != "" { | |
| 145 | 148 | config.General.StorageDir = val | |
| 146 | - | fmt.Printf("Using environment variable GORDON_STORAGE_DIR: %s\n", val) | |
| 149 | + | if printLogs { | |
| 150 | + | fmt.Printf("Using environment variable GORDON_STORAGE_DIR: %s\n", val) | |
| 151 | + | } | |
| 147 | 152 | } | |
| 148 | 153 | if val := os.Getenv("GORDON_TOKEN"); val != "" { | |
| 149 | 154 | config.General.Token = val | |
| 150 | - | fmt.Printf("Using environment variable GORDON_TOKEN\n") | |
| 155 | + | if printLogs { | |
| 156 | + | fmt.Printf("Using environment variable GORDON_TOKEN\n") | |
| 157 | + | } | |
| 151 | 158 | } | |
| 152 | 159 | ||
| 153 | 160 | // HTTP Configuration | |
| 154 | 161 | if val := os.Getenv("GORDON_HTTP_PORT"); val != "" { | |
| 155 | 162 | config.Http.Port = val | |
| 156 | - | fmt.Printf("Using environment variable GORDON_HTTP_PORT: %s\n", val) | |
| 163 | + | if printLogs { | |
| 164 | + | fmt.Printf("Using environment variable GORDON_HTTP_PORT: %s\n", val) | |
| 165 | + | } | |
| 157 | 166 | } | |
| 158 | 167 | if val := os.Getenv("GORDON_HTTP_DOMAIN"); val != "" { | |
| 159 | 168 | config.Http.Domain = val | |
| 160 | - | fmt.Printf("Using environment variable GORDON_HTTP_DOMAIN: %s\n", val) | |
| 169 | + | if printLogs { | |
| 170 | + | fmt.Printf("Using environment variable GORDON_HTTP_DOMAIN: %s\n", val) | |
| 171 | + | } | |
| 161 | 172 | } | |
| 162 | 173 | if val := os.Getenv("GORDON_HTTP_SUBDOMAIN"); val != "" { | |
| 163 | 174 | config.Http.SubDomain = val | |
| 164 | - | fmt.Printf("Using environment variable GORDON_HTTP_SUBDOMAIN: %s\n", val) | |
| 175 | + | if printLogs { | |
| 176 | + | fmt.Printf("Using environment variable GORDON_HTTP_SUBDOMAIN: %s\n", val) | |
| 177 | + | } | |
| 165 | 178 | } | |
| 166 | 179 | if val := os.Getenv("GORDON_HTTP_BACKEND_URL"); val != "" { | |
| 167 | 180 | config.Http.BackendURL = val | |
| 168 | - | fmt.Printf("Using environment variable GORDON_HTTP_BACKEND_URL: %s\n", val) | |
| 181 | + | if printLogs { | |
| 182 | + | fmt.Printf("Using environment variable GORDON_HTTP_BACKEND_URL: %s\n", val) | |
| 183 | + | } | |
| 169 | 184 | } | |
| 170 | 185 | if val := os.Getenv("GORDON_HTTP_HTTPS"); val != "" { | |
| 171 | 186 | config.Http.Https = strings.ToLower(val) == "true" | |
| 172 | - | fmt.Printf("Using environment variable GORDON_HTTP_HTTPS: %t\n", config.Http.Https) | |
| 187 | + | if printLogs { | |
| 188 | + | fmt.Printf("Using environment variable GORDON_HTTP_HTTPS: %t\n", config.Http.Https) | |
| 189 | + | } | |
| 173 | 190 | } | |
| 174 | 191 | ||
| 175 | 192 | // Admin Configuration | |
| 176 | 193 | if val := os.Getenv("GORDON_ADMIN_PATH"); val != "" { | |
| 177 | 194 | config.Admin.Path = val | |
| 178 | - | fmt.Printf("Using environment variable GORDON_ADMIN_PATH: %s\n", val) | |
| 195 | + | if printLogs { | |
| 196 | + | fmt.Printf("Using environment variable GORDON_ADMIN_PATH: %s\n", val) | |
| 197 | + | } | |
| 179 | 198 | } | |
| 180 | 199 | ||
| 181 | 200 | // Container Engine Configuration | |
| 182 | 201 | if val := os.Getenv("GORDON_CONTAINER_SOCK"); val != "" { | |
| 183 | 202 | config.ContainerEngine.Sock = val | |
| 184 | - | fmt.Printf("Using environment variable GORDON_CONTAINER_SOCK: %s\n", val) | |
| 203 | + | if printLogs { | |
| 204 | + | fmt.Printf("Using environment variable GORDON_CONTAINER_SOCK: %s\n", val) | |
| 205 | + | } | |
| 185 | 206 | } | |
| 186 | 207 | if val := os.Getenv("GORDON_CONTAINER_PODMAN_SOCK"); val != "" { | |
| 187 | 208 | config.ContainerEngine.PodmanSock = val | |
| 188 | - | fmt.Printf("Using environment variable GORDON_CONTAINER_PODMAN_SOCK: %s\n", val) | |
| 209 | + | if printLogs { | |
| 210 | + | fmt.Printf("Using environment variable GORDON_CONTAINER_PODMAN_SOCK: %s\n", val) | |
| 211 | + | } | |
| 189 | 212 | } | |
| 190 | 213 | if val := os.Getenv("GORDON_CONTAINER_PODMAN"); val != "" { | |
| 191 | 214 | config.ContainerEngine.Podman = strings.ToLower(val) == "true" | |
| 192 | - | fmt.Printf("Using environment variable GORDON_CONTAINER_PODMAN: %t\n", config.ContainerEngine.Podman) | |
| 215 | + | if printLogs { | |
| 216 | + | fmt.Printf("Using environment variable GORDON_CONTAINER_PODMAN: %t\n", config.ContainerEngine.Podman) | |
| 217 | + | } | |
| 193 | 218 | ||
| 194 | 219 | // If using Podman, and Sock is empty but PodmanSock is set, use PodmanSock for Sock field | |
| 195 | 220 | if config.ContainerEngine.Podman && config.ContainerEngine.Sock == "" && config.ContainerEngine.PodmanSock != "" { | |
| 196 | 221 | config.ContainerEngine.Sock = config.ContainerEngine.PodmanSock | |
| 197 | - | fmt.Printf("Setting ContainerEngine.Sock to PodmanSock value: %s\n", config.ContainerEngine.Sock) | |
| 222 | + | if printLogs { | |
| 223 | + | fmt.Printf("Setting ContainerEngine.Sock to PodmanSock value: %s\n", config.ContainerEngine.Sock) | |
| 224 | + | } | |
| 198 | 225 | } | |
| 199 | 226 | } | |
| 200 | 227 | if val := os.Getenv("GORDON_CONTAINER_NETWORK"); val != "" { | |
| 201 | 228 | config.ContainerEngine.Network = val | |
| 202 | - | fmt.Printf("Using environment variable GORDON_CONTAINER_NETWORK: %s\n", val) | |
| 229 | + | if printLogs { | |
| 230 | + | fmt.Printf("Using environment variable GORDON_CONTAINER_NETWORK: %s\n", val) | |
| 231 | + | } | |
| 203 | 232 | } | |
| 204 | 233 | ||
| 205 | 234 | // If using Docker socket with podman-compose mounting | |
| 206 | 235 | if config.ContainerEngine.Sock == "" && fileExists("/var/run/docker.sock") { | |
| 207 | 236 | config.ContainerEngine.Sock = "/var/run/docker.sock" | |
| 208 | - | fmt.Printf("Docker socket found at /var/run/docker.sock, using it\n") | |
| 237 | + | if printLogs { | |
| 238 | + | fmt.Printf("Docker socket found at /var/run/docker.sock, using it\n") | |
| 239 | + | } | |
| 209 | 240 | } | |
| 210 | 241 | ||
| 211 | 242 | // Reverse Proxy Configuration | |
| 212 | 243 | if val := os.Getenv("GORDON_PROXY_PORT"); val != "" { | |
| 213 | 244 | config.ReverseProxy.Port = val | |
| 214 | - | fmt.Printf("Using environment variable GORDON_PROXY_PORT: %s\n", val) | |
| 245 | + | if printLogs { | |
| 246 | + | fmt.Printf("Using environment variable GORDON_PROXY_PORT: %s\n", val) | |
| 247 | + | } | |
| 215 | 248 | } | |
| 216 | 249 | if val := os.Getenv("GORDON_PROXY_HTTP_PORT"); val != "" { | |
| 217 | 250 | config.ReverseProxy.HttpPort = val | |
| 218 | - | fmt.Printf("Using environment variable GORDON_PROXY_HTTP_PORT: %s\n", val) | |
| 251 | + | if printLogs { | |
| 252 | + | fmt.Printf("Using environment variable GORDON_PROXY_HTTP_PORT: %s\n", val) | |
| 253 | + | } | |
| 219 | 254 | } | |
| 220 | 255 | if val := os.Getenv("GORDON_PROXY_CERT_DIR"); val != "" { | |
| 221 | 256 | config.ReverseProxy.CertDir = val | |
| 222 | - | fmt.Printf("Using environment variable GORDON_PROXY_CERT_DIR: %s\n", val) | |
| 257 | + | if printLogs { | |
| 258 | + | fmt.Printf("Using environment variable GORDON_PROXY_CERT_DIR: %s\n", val) | |
| 259 | + | } | |
| 223 | 260 | } | |
| 224 | 261 | if val := os.Getenv("GORDON_PROXY_AUTO_RENEW"); val != "" { | |
| 225 | 262 | config.ReverseProxy.AutoRenew = strings.ToLower(val) == "true" | |
| 226 | - | fmt.Printf("Using environment variable GORDON_PROXY_AUTO_RENEW: %t\n", config.ReverseProxy.AutoRenew) | |
| 263 | + | if printLogs { | |
| 264 | + | fmt.Printf("Using environment variable GORDON_PROXY_AUTO_RENEW: %t\n", config.ReverseProxy.AutoRenew) | |
| 265 | + | } | |
| 227 | 266 | } | |
| 228 | 267 | if val := os.Getenv("GORDON_PROXY_RENEW_BEFORE"); val != "" { | |
| 229 | 268 | if i, err := strconv.Atoi(val); err == nil { | |
| 230 | 269 | config.ReverseProxy.RenewBefore = i | |
| 231 | - | fmt.Printf("Using environment variable GORDON_PROXY_RENEW_BEFORE: %d\n", i) | |
| 270 | + | if printLogs { | |
| 271 | + | fmt.Printf("Using environment variable GORDON_PROXY_RENEW_BEFORE: %d\n", i) | |
| 272 | + | } | |
| 232 | 273 | } | |
| 233 | 274 | } | |
| 234 | 275 | if val := os.Getenv("GORDON_PROXY_LETSENCRYPT_MODE"); val != "" { | |
| 235 | 276 | config.ReverseProxy.LetsEncryptMode = val | |
| 236 | - | fmt.Printf("Using environment variable GORDON_PROXY_LETSENCRYPT_MODE: %s\n", val) | |
| 277 | + | if printLogs { | |
| 278 | + | fmt.Printf("Using environment variable GORDON_PROXY_LETSENCRYPT_MODE: %s\n", val) | |
| 279 | + | } | |
| 237 | 280 | } | |
| 238 | 281 | if val := os.Getenv("GORDON_PROXY_EMAIL"); val != "" { | |
| 239 | 282 | config.ReverseProxy.Email = val | |
| 240 | - | fmt.Printf("Using environment variable GORDON_PROXY_EMAIL: %s\n", val) | |
| 283 | + | if printLogs { | |
| 284 | + | fmt.Printf("Using environment variable GORDON_PROXY_EMAIL: %s\n", val) | |
| 285 | + | } | |
| 241 | 286 | } | |
| 242 | 287 | if val := os.Getenv("GORDON_PROXY_CACHE_SIZE"); val != "" { | |
| 243 | 288 | if i, err := strconv.Atoi(val); err == nil { | |
| 244 | 289 | config.ReverseProxy.CacheSize = i | |
| 245 | - | fmt.Printf("Using environment variable GORDON_PROXY_CACHE_SIZE: %d\n", i) | |
| 290 | + | if printLogs { | |
| 291 | + | fmt.Printf("Using environment variable GORDON_PROXY_CACHE_SIZE: %d\n", i) | |
| 292 | + | } | |
| 246 | 293 | } | |
| 247 | 294 | } | |
| 248 | 295 | if val := os.Getenv("GORDON_PROXY_GRACE_PERIOD"); val != "" { | |
| 249 | 296 | if i, err := strconv.Atoi(val); err == nil { | |
| 250 | 297 | config.ReverseProxy.GracePeriod = i | |
| 251 | - | fmt.Printf("Using environment variable GORDON_PROXY_GRACE_PERIOD: %d\n", i) | |
| 298 | + | if printLogs { | |
| 299 | + | fmt.Printf("Using environment variable GORDON_PROXY_GRACE_PERIOD: %d\n", i) | |
| 300 | + | } | |
| 252 | 301 | } | |
| 253 | 302 | } | |
| 254 | 303 | } | |
| @@ -268,6 +317,11 @@ func (config *Config) LoadConfig() (*Config, error) { | |||
| 268 | 317 | configFilePath = filepath.Join(configDir, "config.yml") | |
| 269 | 318 | } | |
| 270 | 319 | ||
| 320 | + | // Add an indicator if this is not the first time we're loading config | |
| 321 | + | if configLogsPrinted { | |
| 322 | + | fmt.Println("Reloading configuration (already loaded previously)...") | |
| 323 | + | } | |
| 324 | + | ||
| 271 | 325 | configExists := true | |
| 272 | 326 | _, err = os.Stat(configFilePath) | |
| 273 | 327 | if errors.Is(err, fs.ErrNotExist) { | |
| @@ -339,8 +393,11 @@ func (config *Config) LoadConfig() (*Config, error) { | |||
| 339 | 393 | ||
| 340 | 394 | // Only load environment variables once, after either creating a new config or reading an existing one | |
| 341 | 395 | if docker.IsRunningInContainer() { | |
| 396 | + | // Only print logs if we haven't printed them before | |
| 397 | + | shouldPrintLogs := !configLogsPrinted | |
| 342 | 398 | fmt.Println("Running in container, overriding configuration with environment variables...") | |
| 343 | - | loadConfigFromEnv(config) | |
| 399 | + | loadConfigFromEnv(config, shouldPrintLogs) | |
| 400 | + | configLogsPrinted = true | |
| 344 | 401 | } | |
| 345 | 402 | ||
| 346 | 403 | // Load run env | |
| @@ -359,12 +416,18 @@ func (config *Config) LoadConfig() (*Config, error) { | |||
| 359 | 416 | } | |
| 360 | 417 | ||
| 361 | 418 | // Debug output to verify config was loaded correctly | |
| 362 | - | fmt.Printf("Loaded configuration from %s\n", configFilePath) | |
| 363 | - | fmt.Printf("Container engine config - Docker socket: %s, Podman socket: %s, Using Podman: %t, Network: %s\n", | |
| 364 | - | config.ContainerEngine.Sock, | |
| 365 | - | config.ContainerEngine.PodmanSock, | |
| 366 | - | config.ContainerEngine.Podman, | |
| 367 | - | config.ContainerEngine.Network) | |
| 419 | + | // Only print this debug information if we're also printing the env var logs | |
| 420 | + | if !configLogsPrinted { | |
| 421 | + | fmt.Printf("Loaded configuration from %s\n", configFilePath) | |
| 422 | + | fmt.Printf("Container engine config - Docker socket: %s, Podman socket: %s, Using Podman: %t, Network: %s\n", | |
| 423 | + | config.ContainerEngine.Sock, | |
| 424 | + | config.ContainerEngine.PodmanSock, | |
| 425 | + | config.ContainerEngine.Podman, | |
| 426 | + | config.ContainerEngine.Network) | |
| 427 | + | } else { | |
| 428 | + | // Just print a simplified message | |
| 429 | + | fmt.Printf("Loaded configuration from %s\n", configFilePath) | |
| 430 | + | } | |
| 368 | 431 | ||
| 369 | 432 | return config, nil | |
| 370 | 433 | } | |
| @@ -80,6 +80,9 @@ var ( | |||||
| 80 | gracePeriod = 30 // seconds | 80 | gracePeriod = 30 // seconds | ||
| 81 | ) | 81 | ) | ||
| 82 | 82 | ||||
| 83 | + | // Track whether config logs have been printed already | |||
| 84 | + | var configLogsPrinted bool = false | |||
| 85 | + | ||||
| 83 | func getConfigDir() (string, error) { | 86 | func getConfigDir() (string, error) { | ||
| 84 | var configDir string | 87 | var configDir string | ||
| 85 | 88 | ||||
| @@ -139,116 +142,162 @@ func readAndUnmarshalConfig(fs fs.FS, filePath string, config *Config) error { | |||||
| 139 | } | 142 | } | ||
| 140 | 143 | ||||
| 141 | // loadConfigFromEnv loads configuration from environment variables | 144 | // loadConfigFromEnv loads configuration from environment variables | ||
| 142 | - | func loadConfigFromEnv(config *Config) { | 145 | + | func loadConfigFromEnv(config *Config, printLogs bool) { |
| 143 | // General Configuration | 146 | // General Configuration | ||
| 144 | if val := os.Getenv("GORDON_STORAGE_DIR"); val != "" { | 147 | if val := os.Getenv("GORDON_STORAGE_DIR"); val != "" { | ||
| 145 | config.General.StorageDir = val | 148 | config.General.StorageDir = val | ||
| 146 | - | fmt.Printf("Using environment variable GORDON_STORAGE_DIR: %s\n", val) | 149 | + | if printLogs { |
| 150 | + | fmt.Printf("Using environment variable GORDON_STORAGE_DIR: %s\n", val) | |||
| 151 | + | } | |||
| 147 | } | 152 | } | ||
| 148 | if val := os.Getenv("GORDON_TOKEN"); val != "" { | 153 | if val := os.Getenv("GORDON_TOKEN"); val != "" { | ||
| 149 | config.General.Token = val | 154 | config.General.Token = val | ||
| 150 | - | fmt.Printf("Using environment variable GORDON_TOKEN\n") | 155 | + | if printLogs { |
| 156 | + | fmt.Printf("Using environment variable GORDON_TOKEN\n") | |||
| 157 | + | } | |||
| 151 | } | 158 | } | ||
| 152 | 159 | ||||
| 153 | // HTTP Configuration | 160 | // HTTP Configuration | ||
| 154 | if val := os.Getenv("GORDON_HTTP_PORT"); val != "" { | 161 | if val := os.Getenv("GORDON_HTTP_PORT"); val != "" { | ||
| 155 | config.Http.Port = val | 162 | config.Http.Port = val | ||
| 156 | - | fmt.Printf("Using environment variable GORDON_HTTP_PORT: %s\n", val) | 163 | + | if printLogs { |
| 164 | + | fmt.Printf("Using environment variable GORDON_HTTP_PORT: %s\n", val) | |||
| 165 | + | } | |||
| 157 | } | 166 | } | ||
| 158 | if val := os.Getenv("GORDON_HTTP_DOMAIN"); val != "" { | 167 | if val := os.Getenv("GORDON_HTTP_DOMAIN"); val != "" { | ||
| 159 | config.Http.Domain = val | 168 | config.Http.Domain = val | ||
| 160 | - | fmt.Printf("Using environment variable GORDON_HTTP_DOMAIN: %s\n", val) | 169 | + | if printLogs { |
| 170 | + | fmt.Printf("Using environment variable GORDON_HTTP_DOMAIN: %s\n", val) | |||
| 171 | + | } | |||
| 161 | } | 172 | } | ||
| 162 | if val := os.Getenv("GORDON_HTTP_SUBDOMAIN"); val != "" { | 173 | if val := os.Getenv("GORDON_HTTP_SUBDOMAIN"); val != "" { | ||
| 163 | config.Http.SubDomain = val | 174 | config.Http.SubDomain = val | ||
| 164 | - | fmt.Printf("Using environment variable GORDON_HTTP_SUBDOMAIN: %s\n", val) | 175 | + | if printLogs { |
| 176 | + | fmt.Printf("Using environment variable GORDON_HTTP_SUBDOMAIN: %s\n", val) | |||
| 177 | + | } | |||
| 165 | } | 178 | } | ||
| 166 | if val := os.Getenv("GORDON_HTTP_BACKEND_URL"); val != "" { | 179 | if val := os.Getenv("GORDON_HTTP_BACKEND_URL"); val != "" { | ||
| 167 | config.Http.BackendURL = val | 180 | config.Http.BackendURL = val | ||
| 168 | - | fmt.Printf("Using environment variable GORDON_HTTP_BACKEND_URL: %s\n", val) | 181 | + | if printLogs { |
| 182 | + | fmt.Printf("Using environment variable GORDON_HTTP_BACKEND_URL: %s\n", val) | |||
| 183 | + | } | |||
| 169 | } | 184 | } | ||
| 170 | if val := os.Getenv("GORDON_HTTP_HTTPS"); val != "" { | 185 | if val := os.Getenv("GORDON_HTTP_HTTPS"); val != "" { | ||
| 171 | config.Http.Https = strings.ToLower(val) == "true" | 186 | config.Http.Https = strings.ToLower(val) == "true" | ||
| 172 | - | fmt.Printf("Using environment variable GORDON_HTTP_HTTPS: %t\n", config.Http.Https) | 187 | + | if printLogs { |
| 188 | + | fmt.Printf("Using environment variable GORDON_HTTP_HTTPS: %t\n", config.Http.Https) | |||
| 189 | + | } | |||
| 173 | } | 190 | } | ||
| 174 | 191 | ||||
| 175 | // Admin Configuration | 192 | // Admin Configuration | ||
| 176 | if val := os.Getenv("GORDON_ADMIN_PATH"); val != "" { | 193 | if val := os.Getenv("GORDON_ADMIN_PATH"); val != "" { | ||
| 177 | config.Admin.Path = val | 194 | config.Admin.Path = val | ||
| 178 | - | fmt.Printf("Using environment variable GORDON_ADMIN_PATH: %s\n", val) | 195 | + | if printLogs { |
| 196 | + | fmt.Printf("Using environment variable GORDON_ADMIN_PATH: %s\n", val) | |||
| 197 | + | } | |||
| 179 | } | 198 | } | ||
| 180 | 199 | ||||
| 181 | // Container Engine Configuration | 200 | // Container Engine Configuration | ||
| 182 | if val := os.Getenv("GORDON_CONTAINER_SOCK"); val != "" { | 201 | if val := os.Getenv("GORDON_CONTAINER_SOCK"); val != "" { | ||
| 183 | config.ContainerEngine.Sock = val | 202 | config.ContainerEngine.Sock = val | ||
| 184 | - | fmt.Printf("Using environment variable GORDON_CONTAINER_SOCK: %s\n", val) | 203 | + | if printLogs { |
| 204 | + | fmt.Printf("Using environment variable GORDON_CONTAINER_SOCK: %s\n", val) | |||
| 205 | + | } | |||
| 185 | } | 206 | } | ||
| 186 | if val := os.Getenv("GORDON_CONTAINER_PODMAN_SOCK"); val != "" { | 207 | if val := os.Getenv("GORDON_CONTAINER_PODMAN_SOCK"); val != "" { | ||
| 187 | config.ContainerEngine.PodmanSock = val | 208 | config.ContainerEngine.PodmanSock = val | ||
| 188 | - | fmt.Printf("Using environment variable GORDON_CONTAINER_PODMAN_SOCK: %s\n", val) | 209 | + | if printLogs { |
| 210 | + | fmt.Printf("Using environment variable GORDON_CONTAINER_PODMAN_SOCK: %s\n", val) | |||
| 211 | + | } | |||
| 189 | } | 212 | } | ||
| 190 | if val := os.Getenv("GORDON_CONTAINER_PODMAN"); val != "" { | 213 | if val := os.Getenv("GORDON_CONTAINER_PODMAN"); val != "" { | ||
| 191 | config.ContainerEngine.Podman = strings.ToLower(val) == "true" | 214 | config.ContainerEngine.Podman = strings.ToLower(val) == "true" | ||
| 192 | - | fmt.Printf("Using environment variable GORDON_CONTAINER_PODMAN: %t\n", config.ContainerEngine.Podman) | 215 | + | if printLogs { |
| 216 | + | fmt.Printf("Using environment variable GORDON_CONTAINER_PODMAN: %t\n", config.ContainerEngine.Podman) | |||
| 217 | + | } | |||
| 193 | 218 | ||||
| 194 | // If using Podman, and Sock is empty but PodmanSock is set, use PodmanSock for Sock field | 219 | // If using Podman, and Sock is empty but PodmanSock is set, use PodmanSock for Sock field | ||
| 195 | if config.ContainerEngine.Podman && config.ContainerEngine.Sock == "" && config.ContainerEngine.PodmanSock != "" { | 220 | if config.ContainerEngine.Podman && config.ContainerEngine.Sock == "" && config.ContainerEngine.PodmanSock != "" { | ||
| 196 | config.ContainerEngine.Sock = config.ContainerEngine.PodmanSock | 221 | config.ContainerEngine.Sock = config.ContainerEngine.PodmanSock | ||
| 197 | - | fmt.Printf("Setting ContainerEngine.Sock to PodmanSock value: %s\n", config.ContainerEngine.Sock) | 222 | + | if printLogs { |
| 223 | + | fmt.Printf("Setting ContainerEngine.Sock to PodmanSock value: %s\n", config.ContainerEngine.Sock) | |||
| 224 | + | } | |||
| 198 | } | 225 | } | ||
| 199 | } | 226 | } | ||
| 200 | if val := os.Getenv("GORDON_CONTAINER_NETWORK"); val != "" { | 227 | if val := os.Getenv("GORDON_CONTAINER_NETWORK"); val != "" { | ||
| 201 | config.ContainerEngine.Network = val | 228 | config.ContainerEngine.Network = val | ||
| 202 | - | fmt.Printf("Using environment variable GORDON_CONTAINER_NETWORK: %s\n", val) | 229 | + | if printLogs { |
| 230 | + | fmt.Printf("Using environment variable GORDON_CONTAINER_NETWORK: %s\n", val) | |||
| 231 | + | } | |||
| 203 | } | 232 | } | ||
| 204 | 233 | ||||
| 205 | // If using Docker socket with podman-compose mounting | 234 | // If using Docker socket with podman-compose mounting | ||
| 206 | if config.ContainerEngine.Sock == "" && fileExists("/var/run/docker.sock") { | 235 | if config.ContainerEngine.Sock == "" && fileExists("/var/run/docker.sock") { | ||
| 207 | config.ContainerEngine.Sock = "/var/run/docker.sock" | 236 | config.ContainerEngine.Sock = "/var/run/docker.sock" | ||
| 208 | - | fmt.Printf("Docker socket found at /var/run/docker.sock, using it\n") | 237 | + | if printLogs { |
| 238 | + | fmt.Printf("Docker socket found at /var/run/docker.sock, using it\n") | |||
| 239 | + | } | |||
| 209 | } | 240 | } | ||
| 210 | 241 | ||||
| 211 | // Reverse Proxy Configuration | 242 | // Reverse Proxy Configuration | ||
| 212 | if val := os.Getenv("GORDON_PROXY_PORT"); val != "" { | 243 | if val := os.Getenv("GORDON_PROXY_PORT"); val != "" { | ||
| 213 | config.ReverseProxy.Port = val | 244 | config.ReverseProxy.Port = val | ||
| 214 | - | fmt.Printf("Using environment variable GORDON_PROXY_PORT: %s\n", val) | 245 | + | if printLogs { |
| 246 | + | fmt.Printf("Using environment variable GORDON_PROXY_PORT: %s\n", val) | |||
| 247 | + | } | |||
| 215 | } | 248 | } | ||
| 216 | if val := os.Getenv("GORDON_PROXY_HTTP_PORT"); val != "" { | 249 | if val := os.Getenv("GORDON_PROXY_HTTP_PORT"); val != "" { | ||
| 217 | config.ReverseProxy.HttpPort = val | 250 | config.ReverseProxy.HttpPort = val | ||
| 218 | - | fmt.Printf("Using environment variable GORDON_PROXY_HTTP_PORT: %s\n", val) | 251 | + | if printLogs { |
| 252 | + | fmt.Printf("Using environment variable GORDON_PROXY_HTTP_PORT: %s\n", val) | |||
| 253 | + | } | |||
| 219 | } | 254 | } | ||
| 220 | if val := os.Getenv("GORDON_PROXY_CERT_DIR"); val != "" { | 255 | if val := os.Getenv("GORDON_PROXY_CERT_DIR"); val != "" { | ||
| 221 | config.ReverseProxy.CertDir = val | 256 | config.ReverseProxy.CertDir = val | ||
| 222 | - | fmt.Printf("Using environment variable GORDON_PROXY_CERT_DIR: %s\n", val) | 257 | + | if printLogs { |
| 258 | + | fmt.Printf("Using environment variable GORDON_PROXY_CERT_DIR: %s\n", val) | |||
| 259 | + | } | |||
| 223 | } | 260 | } | ||
| 224 | if val := os.Getenv("GORDON_PROXY_AUTO_RENEW"); val != "" { | 261 | if val := os.Getenv("GORDON_PROXY_AUTO_RENEW"); val != "" { | ||
| 225 | config.ReverseProxy.AutoRenew = strings.ToLower(val) == "true" | 262 | config.ReverseProxy.AutoRenew = strings.ToLower(val) == "true" | ||
| 226 | - | fmt.Printf("Using environment variable GORDON_PROXY_AUTO_RENEW: %t\n", config.ReverseProxy.AutoRenew) | 263 | + | if printLogs { |
| 264 | + | fmt.Printf("Using environment variable GORDON_PROXY_AUTO_RENEW: %t\n", config.ReverseProxy.AutoRenew) | |||
| 265 | + | } | |||
| 227 | } | 266 | } | ||
| 228 | if val := os.Getenv("GORDON_PROXY_RENEW_BEFORE"); val != "" { | 267 | if val := os.Getenv("GORDON_PROXY_RENEW_BEFORE"); val != "" { | ||
| 229 | if i, err := strconv.Atoi(val); err == nil { | 268 | if i, err := strconv.Atoi(val); err == nil { | ||
| 230 | config.ReverseProxy.RenewBefore = i | 269 | config.ReverseProxy.RenewBefore = i | ||
| 231 | - | fmt.Printf("Using environment variable GORDON_PROXY_RENEW_BEFORE: %d\n", i) | 270 | + | if printLogs { |
| 271 | + | fmt.Printf("Using environment variable GORDON_PROXY_RENEW_BEFORE: %d\n", i) | |||
| 272 | + | } | |||
| 232 | } | 273 | } | ||
| 233 | } | 274 | } | ||
| 234 | if val := os.Getenv("GORDON_PROXY_LETSENCRYPT_MODE"); val != "" { | 275 | if val := os.Getenv("GORDON_PROXY_LETSENCRYPT_MODE"); val != "" { | ||
| 235 | config.ReverseProxy.LetsEncryptMode = val | 276 | config.ReverseProxy.LetsEncryptMode = val | ||
| 236 | - | fmt.Printf("Using environment variable GORDON_PROXY_LETSENCRYPT_MODE: %s\n", val) | 277 | + | if printLogs { |
| 278 | + | fmt.Printf("Using environment variable GORDON_PROXY_LETSENCRYPT_MODE: %s\n", val) | |||
| 279 | + | } | |||
| 237 | } | 280 | } | ||
| 238 | if val := os.Getenv("GORDON_PROXY_EMAIL"); val != "" { | 281 | if val := os.Getenv("GORDON_PROXY_EMAIL"); val != "" { | ||
| 239 | config.ReverseProxy.Email = val | 282 | config.ReverseProxy.Email = val | ||
| 240 | - | fmt.Printf("Using environment variable GORDON_PROXY_EMAIL: %s\n", val) | 283 | + | if printLogs { |
| 284 | + | fmt.Printf("Using environment variable GORDON_PROXY_EMAIL: %s\n", val) | |||
| 285 | + | } | |||
| 241 | } | 286 | } | ||
| 242 | if val := os.Getenv("GORDON_PROXY_CACHE_SIZE"); val != "" { | 287 | if val := os.Getenv("GORDON_PROXY_CACHE_SIZE"); val != "" { | ||
| 243 | if i, err := strconv.Atoi(val); err == nil { | 288 | if i, err := strconv.Atoi(val); err == nil { | ||
| 244 | config.ReverseProxy.CacheSize = i | 289 | config.ReverseProxy.CacheSize = i | ||
| 245 | - | fmt.Printf("Using environment variable GORDON_PROXY_CACHE_SIZE: %d\n", i) | 290 | + | if printLogs { |
| 291 | + | fmt.Printf("Using environment variable GORDON_PROXY_CACHE_SIZE: %d\n", i) | |||
| 292 | + | } | |||
| 246 | } | 293 | } | ||
| 247 | } | 294 | } | ||
| 248 | if val := os.Getenv("GORDON_PROXY_GRACE_PERIOD"); val != "" { | 295 | if val := os.Getenv("GORDON_PROXY_GRACE_PERIOD"); val != "" { | ||
| 249 | if i, err := strconv.Atoi(val); err == nil { | 296 | if i, err := strconv.Atoi(val); err == nil { | ||
| 250 | config.ReverseProxy.GracePeriod = i | 297 | config.ReverseProxy.GracePeriod = i | ||
| 251 | - | fmt.Printf("Using environment variable GORDON_PROXY_GRACE_PERIOD: %d\n", i) | 298 | + | if printLogs { |
| 299 | + | fmt.Printf("Using environment variable GORDON_PROXY_GRACE_PERIOD: %d\n", i) | |||
| 300 | + | } | |||
| 252 | } | 301 | } | ||
| 253 | } | 302 | } | ||
| 254 | } | 303 | } | ||
| @@ -268,6 +317,11 @@ func (config *Config) LoadConfig() (*Config, error) { | |||||
| 268 | configFilePath = filepath.Join(configDir, "config.yml") | 317 | configFilePath = filepath.Join(configDir, "config.yml") | ||
| 269 | } | 318 | } | ||
| 270 | 319 | ||||
| 320 | + | // Add an indicator if this is not the first time we're loading config | |||
| 321 | + | if configLogsPrinted { | |||
| 322 | + | fmt.Println("Reloading configuration (already loaded previously)...") | |||
| 323 | + | } | |||
| 324 | + | ||||
| 271 | configExists := true | 325 | configExists := true | ||
| 272 | _, err = os.Stat(configFilePath) | 326 | _, err = os.Stat(configFilePath) | ||
| 273 | if errors.Is(err, fs.ErrNotExist) { | 327 | if errors.Is(err, fs.ErrNotExist) { | ||
| @@ -339,8 +393,11 @@ func (config *Config) LoadConfig() (*Config, error) { | |||||
| 339 | 393 | ||||
| 340 | // Only load environment variables once, after either creating a new config or reading an existing one | 394 | // Only load environment variables once, after either creating a new config or reading an existing one | ||
| 341 | if docker.IsRunningInContainer() { | 395 | if docker.IsRunningInContainer() { | ||
| 396 | + | // Only print logs if we haven't printed them before | |||
| 397 | + | shouldPrintLogs := !configLogsPrinted | |||
| 342 | fmt.Println("Running in container, overriding configuration with environment variables...") | 398 | fmt.Println("Running in container, overriding configuration with environment variables...") | ||
| 343 | - | loadConfigFromEnv(config) | 399 | + | loadConfigFromEnv(config, shouldPrintLogs) |
| 400 | + | configLogsPrinted = true | |||
| 344 | } | 401 | } | ||
| 345 | 402 | ||||
| 346 | // Load run env | 403 | // Load run env | ||
| @@ -359,12 +416,18 @@ func (config *Config) LoadConfig() (*Config, error) { | |||||
| 359 | } | 416 | } | ||
| 360 | 417 | ||||
| 361 | // Debug output to verify config was loaded correctly | 418 | // Debug output to verify config was loaded correctly | ||
| 362 | - | fmt.Printf("Loaded configuration from %s\n", configFilePath) | 419 | + | // Only print this debug information if we're also printing the env var logs |
| 363 | - | fmt.Printf("Container engine config - Docker socket: %s, Podman socket: %s, Using Podman: %t, Network: %s\n", | 420 | + | if !configLogsPrinted { |
| 364 | - | config.ContainerEngine.Sock, | 421 | + | fmt.Printf("Loaded configuration from %s\n", configFilePath) |
| 365 | - | config.ContainerEngine.PodmanSock, | 422 | + | fmt.Printf("Container engine config - Docker socket: %s, Podman socket: %s, Using Podman: %t, Network: %s\n", |
| 366 | - | config.ContainerEngine.Podman, | 423 | + | config.ContainerEngine.Sock, |
| 367 | - | config.ContainerEngine.Network) | 424 | + | config.ContainerEngine.PodmanSock, |
| 425 | + | config.ContainerEngine.Podman, | |||
| 426 | + | config.ContainerEngine.Network) | |||
| 427 | + | } else { | |||
| 428 | + | // Just print a simplified message | |||
| 429 | + | fmt.Printf("Loaded configuration from %s\n", configFilePath) | |||
| 430 | + | } | |||
| 368 | 431 | ||||
| 369 | return config, nil | 432 | return config, nil | ||
| 370 | } | 433 | } | ||
M internal/proxy/blacklist.go +195 -39
| @@ -21,48 +21,65 @@ type BlacklistConfig struct { | |||
| 21 | 21 | LastMod time.Time `yaml:"-"` // Last modification time | |
| 22 | 22 | mu sync.RWMutex `yaml:"-"` // Mutex for thread safety | |
| 23 | 23 | path string `yaml:"-"` // Path to blacklist file | |
| 24 | + | ||
| 25 | + | // In-memory cache of blocked IPs for quick lookups | |
| 26 | + | blockedCache map[string]bool `yaml:"-"` | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | // MarshalableConfig is a copy of BlacklistConfig without the mutex and other non-serializable fields | |
| 30 | + | // Used to prevent copying mutex when marshaling to YAML | |
| 31 | + | type MarshalableConfig struct { | |
| 32 | + | IPs []string `yaml:"ips"` | |
| 33 | + | Ranges []string `yaml:"ranges"` | |
| 24 | 34 | } | |
| 25 | 35 | ||
| 26 | - | // NewBlacklist creates a new IP blacklist | |
| 36 | + | // NewBlacklist creates a new BlacklistConfig from a file | |
| 27 | 37 | func NewBlacklist(path string) (*BlacklistConfig, error) { | |
| 38 | + | // Create the blacklist config | |
| 28 | 39 | blacklist := &BlacklistConfig{ | |
| 29 | - | path: path, | |
| 40 | + | path: path, | |
| 41 | + | blockedCache: make(map[string]bool), | |
| 30 | 42 | } | |
| 31 | 43 | ||
| 32 | - | // Load initial blacklist | |
| 33 | - | if err := blacklist.Load(); err != nil { | |
| 34 | - | // If file doesn't exist, create an empty blacklist | |
| 35 | - | if os.IsNotExist(err) { | |
| 36 | - | log.Info("Blacklist file not found, creating empty blacklist", "path", path) | |
| 37 | - | blacklist.IPs = []string{} | |
| 38 | - | blacklist.Ranges = []string{} | |
| 39 | - | blacklist.Networks = []*net.IPNet{} | |
| 40 | - | ||
| 41 | - | // Create directory if needed | |
| 42 | - | dir := filepath.Dir(path) | |
| 44 | + | // Create the blacklist file if it doesn't exist | |
| 45 | + | if _, err := os.Stat(path); os.IsNotExist(err) { | |
| 46 | + | // Create parent directory if it doesn't exist | |
| 47 | + | dir := filepath.Dir(path) | |
| 48 | + | if _, err := os.Stat(dir); os.IsNotExist(err) { | |
| 43 | 49 | if err := os.MkdirAll(dir, 0755); err != nil { | |
| 44 | - | log.Error("Failed to create directory for blacklist", "dir", dir, "error", err) | |
| 50 | + | return nil, fmt.Errorf("failed to create directory for blacklist: %w", err) | |
| 45 | 51 | } | |
| 52 | + | } | |
| 46 | 53 | ||
| 47 | - | // Write empty blacklist file | |
| 48 | - | emptyConfig := &BlacklistConfig{ | |
| 49 | - | IPs: []string{}, | |
| 50 | - | Ranges: []string{}, | |
| 51 | - | } | |
| 52 | - | data, _ := yaml.Marshal(emptyConfig) | |
| 53 | - | if err := os.WriteFile(path, data, 0644); err != nil { | |
| 54 | - | log.Error("Failed to create empty blacklist file", "path", path, "error", err) | |
| 55 | - | } | |
| 54 | + | // Create the blacklist file with default empty structure | |
| 55 | + | data, err := yaml.Marshal(MarshalableConfig{ | |
| 56 | + | IPs: []string{}, | |
| 57 | + | Ranges: []string{}, | |
| 58 | + | }) | |
| 59 | + | if err != nil { | |
| 60 | + | return nil, fmt.Errorf("failed to marshal default blacklist: %w", err) | |
| 61 | + | } | |
| 56 | 62 | ||
| 57 | - | return blacklist, nil | |
| 63 | + | if err := os.WriteFile(path, data, 0644); err != nil { | |
| 64 | + | return nil, fmt.Errorf("failed to create blacklist file: %w", err) | |
| 58 | 65 | } | |
| 59 | - | return nil, err | |
| 66 | + | ||
| 67 | + | log.Info("Created new blacklist file", "path", path) | |
| 60 | 68 | } | |
| 61 | 69 | ||
| 70 | + | // Load the blacklist | |
| 71 | + | if err := blacklist.Load(); err != nil { | |
| 72 | + | return nil, fmt.Errorf("failed to load blacklist: %w", err) | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | log.Info("Blacklist loaded", | |
| 76 | + | "ips", len(blacklist.IPs), | |
| 77 | + | "ranges", len(blacklist.Ranges)) | |
| 78 | + | ||
| 62 | 79 | return blacklist, nil | |
| 63 | 80 | } | |
| 64 | 81 | ||
| 65 | - | // Load reads the blacklist from disk | |
| 82 | + | // Load reloads the blacklist from the file if it has changed | |
| 66 | 83 | func (b *BlacklistConfig) Load() error { | |
| 67 | 84 | b.mu.Lock() | |
| 68 | 85 | defer b.mu.Unlock() | |
| @@ -87,7 +104,7 @@ func (b *BlacklistConfig) Load() error { | |||
| 87 | 104 | } | |
| 88 | 105 | ||
| 89 | 106 | // Create a temporary config to unmarshal into | |
| 90 | - | var tempConfig BlacklistConfig | |
| 107 | + | var tempConfig MarshalableConfig | |
| 91 | 108 | if err := yaml.Unmarshal(data, &tempConfig); err != nil { | |
| 92 | 109 | return err | |
| 93 | 110 | } | |
| @@ -114,6 +131,9 @@ func (b *BlacklistConfig) Load() error { | |||
| 114 | 131 | b.Networks = networks | |
| 115 | 132 | b.LastMod = fileInfo.ModTime() | |
| 116 | 133 | ||
| 134 | + | // Rebuild the blocked cache | |
| 135 | + | b.rebuildBlockedCache() | |
| 136 | + | ||
| 117 | 137 | // If the file has actually changed, log about it | |
| 118 | 138 | if fileInfo.ModTime() != b.LastMod { | |
| 119 | 139 | log.Info("Loaded blacklist", | |
| @@ -129,6 +149,22 @@ func (b *BlacklistConfig) Load() error { | |||
| 129 | 149 | return nil | |
| 130 | 150 | } | |
| 131 | 151 | ||
| 152 | + | // rebuildBlockedCache rebuilds the in-memory cache of blocked IPs for quick lookups | |
| 153 | + | func (b *BlacklistConfig) rebuildBlockedCache() { | |
| 154 | + | // Create a new map to hold all blocked IPs | |
| 155 | + | newCache := make(map[string]bool, len(b.IPs)) | |
| 156 | + | ||
| 157 | + | // Add direct IPs | |
| 158 | + | for _, ip := range b.IPs { | |
| 159 | + | newCache[ip] = true | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | // Replace the cache atomically | |
| 163 | + | b.blockedCache = newCache | |
| 164 | + | ||
| 165 | + | log.Debug("Rebuilt blocked IP cache", "direct_ips", len(b.blockedCache)) | |
| 166 | + | } | |
| 167 | + | ||
| 132 | 168 | // IsBlocked checks if an IP is in the blacklist | |
| 133 | 169 | func (b *BlacklistConfig) IsBlocked(ip string) bool { | |
| 134 | 170 | // Try to reload the blacklist first | |
| @@ -139,11 +175,9 @@ func (b *BlacklistConfig) IsBlocked(ip string) bool { | |||
| 139 | 175 | b.mu.RLock() | |
| 140 | 176 | defer b.mu.RUnlock() | |
| 141 | 177 | ||
| 142 | - | // Check direct IP match first (fastest) | |
| 143 | - | for _, blockedIP := range b.IPs { | |
| 144 | - | if ip == blockedIP { | |
| 145 | - | return true | |
| 146 | - | } | |
| 178 | + | // Fast path: Check direct IP match in cache first (fastest) | |
| 179 | + | if b.blockedCache[ip] { | |
| 180 | + | return true | |
| 147 | 181 | } | |
| 148 | 182 | ||
| 149 | 183 | // Parse the IP | |
| @@ -156,6 +190,9 @@ func (b *BlacklistConfig) IsBlocked(ip string) bool { | |||
| 156 | 190 | // Check if IP is in any blocked ranges | |
| 157 | 191 | for _, network := range b.Networks { | |
| 158 | 192 | if network.Contains(parsedIP) { | |
| 193 | + | // Add to cache for faster lookups next time | |
| 194 | + | // This is safe because we have a read lock and the map is only replaced, not modified | |
| 195 | + | b.blockedCache[ip] = true | |
| 159 | 196 | return true | |
| 160 | 197 | } | |
| 161 | 198 | } | |
| @@ -175,7 +212,7 @@ func (b *BlacklistConfig) AddIP(ip string) error { | |||
| 175 | 212 | } | |
| 176 | 213 | ||
| 177 | 214 | // Parse the current data | |
| 178 | - | var config BlacklistConfig | |
| 215 | + | var config MarshalableConfig | |
| 179 | 216 | if err := yaml.Unmarshal(data, &config); err != nil { | |
| 180 | 217 | return err | |
| 181 | 218 | } | |
| @@ -200,21 +237,29 @@ func (b *BlacklistConfig) AddIP(ip string) error { | |||
| 200 | 237 | return err | |
| 201 | 238 | } | |
| 202 | 239 | ||
| 240 | + | // Add to in-memory cache immediately | |
| 241 | + | b.blockedCache[ip] = true | |
| 242 | + | log.Info("Added IP to blacklist", "ip", ip) | |
| 243 | + | ||
| 203 | 244 | // Force a reload | |
| 204 | 245 | return b.Load() | |
| 205 | 246 | } | |
| 206 | 247 | ||
| 207 | 248 | // AddRange adds an IP range to the blacklist immediately | |
| 208 | 249 | func (b *BlacklistConfig) AddRange(cidr string) error { | |
| 209 | - | // Make sure the CIDR is valid | |
| 250 | + | b.mu.Lock() | |
| 251 | + | defer b.mu.Unlock() | |
| 252 | + | ||
| 253 | + | // Validate the CIDR | |
| 254 | + | if !strings.Contains(cidr, "/") { | |
| 255 | + | cidr = cidr + "/32" // Add /32 for single IPs | |
| 256 | + | } | |
| 257 | + | ||
| 210 | 258 | _, _, err := net.ParseCIDR(cidr) | |
| 211 | 259 | if err != nil { | |
| 212 | - | return fmt.Errorf("invalid CIDR: %w", err) | |
| 260 | + | return fmt.Errorf("invalid CIDR format: %w", err) | |
| 213 | 261 | } | |
| 214 | 262 | ||
| 215 | - | b.mu.Lock() | |
| 216 | - | defer b.mu.Unlock() | |
| 217 | - | ||
| 218 | 263 | // Read the current file | |
| 219 | 264 | data, err := os.ReadFile(b.path) | |
| 220 | 265 | if err != nil { | |
| @@ -222,7 +267,7 @@ func (b *BlacklistConfig) AddRange(cidr string) error { | |||
| 222 | 267 | } | |
| 223 | 268 | ||
| 224 | 269 | // Parse the current data | |
| 225 | - | var config BlacklistConfig | |
| 270 | + | var config MarshalableConfig | |
| 226 | 271 | if err := yaml.Unmarshal(data, &config); err != nil { | |
| 227 | 272 | return err | |
| 228 | 273 | } | |
| @@ -247,6 +292,117 @@ func (b *BlacklistConfig) AddRange(cidr string) error { | |||
| 247 | 292 | return err | |
| 248 | 293 | } | |
| 249 | 294 | ||
| 295 | + | log.Info("Added CIDR range to blacklist", "cidr", cidr) | |
| 296 | + | ||
| 297 | + | // Force a reload to update Networks and the cache | |
| 298 | + | return b.Load() | |
| 299 | + | } | |
| 300 | + | ||
| 301 | + | // RemoveIP removes an IP from the blacklist | |
| 302 | + | func (b *BlacklistConfig) RemoveIP(ip string) error { | |
| 303 | + | b.mu.Lock() | |
| 304 | + | defer b.mu.Unlock() | |
| 305 | + | ||
| 306 | + | // Read the current file | |
| 307 | + | data, err := os.ReadFile(b.path) | |
| 308 | + | if err != nil { | |
| 309 | + | return err | |
| 310 | + | } | |
| 311 | + | ||
| 312 | + | // Parse the current data | |
| 313 | + | var config MarshalableConfig | |
| 314 | + | if err := yaml.Unmarshal(data, &config); err != nil { | |
| 315 | + | return err | |
| 316 | + | } | |
| 317 | + | ||
| 318 | + | // Check if the IP is in the list | |
| 319 | + | found := false | |
| 320 | + | newIPs := make([]string, 0, len(config.IPs)) | |
| 321 | + | for _, blockedIP := range config.IPs { | |
| 322 | + | if blockedIP == ip { | |
| 323 | + | found = true | |
| 324 | + | continue // Skip this IP | |
| 325 | + | } | |
| 326 | + | newIPs = append(newIPs, blockedIP) | |
| 327 | + | } | |
| 328 | + | ||
| 329 | + | if !found { | |
| 330 | + | return fmt.Errorf("IP not found in blacklist: %s", ip) | |
| 331 | + | } | |
| 332 | + | ||
| 333 | + | // Update the list | |
| 334 | + | config.IPs = newIPs | |
| 335 | + | ||
| 336 | + | // Write the updated config back to the file | |
| 337 | + | newData, err := yaml.Marshal(config) | |
| 338 | + | if err != nil { | |
| 339 | + | return err | |
| 340 | + | } | |
| 341 | + | ||
| 342 | + | if err := os.WriteFile(b.path, newData, 0644); err != nil { | |
| 343 | + | return err | |
| 344 | + | } | |
| 345 | + | ||
| 346 | + | // Remove from cache | |
| 347 | + | delete(b.blockedCache, ip) | |
| 348 | + | log.Info("Removed IP from blacklist", "ip", ip) | |
| 349 | + | ||
| 250 | 350 | // Force a reload | |
| 251 | 351 | return b.Load() | |
| 252 | 352 | } | |
| 353 | + | ||
| 354 | + | // RemoveRange removes an IP range from the blacklist | |
| 355 | + | func (b *BlacklistConfig) RemoveRange(cidr string) error { | |
| 356 | + | b.mu.Lock() | |
| 357 | + | defer b.mu.Unlock() | |
| 358 | + | ||
| 359 | + | // Normalize CIDR | |
| 360 | + | if !strings.Contains(cidr, "/") { | |
| 361 | + | cidr = cidr + "/32" // Add /32 for single IPs | |
| 362 | + | } | |
| 363 | + | ||
| 364 | + | // Read the current file | |
| 365 | + | data, err := os.ReadFile(b.path) | |
| 366 | + | if err != nil { | |
| 367 | + | return err | |
| 368 | + | } | |
| 369 | + | ||
| 370 | + | // Parse the current data | |
| 371 | + | var config MarshalableConfig | |
| 372 | + | if err := yaml.Unmarshal(data, &config); err != nil { | |
| 373 | + | return err | |
| 374 | + | } | |
| 375 | + | ||
| 376 | + | // Check if the range is in the list | |
| 377 | + | found := false | |
| 378 | + | newRanges := make([]string, 0, len(config.Ranges)) | |
| 379 | + | for _, blockedRange := range config.Ranges { | |
| 380 | + | if blockedRange == cidr { | |
| 381 | + | found = true | |
| 382 | + | continue // Skip this range | |
| 383 | + | } | |
| 384 | + | newRanges = append(newRanges, blockedRange) | |
| 385 | + | } | |
| 386 | + | ||
| 387 | + | if !found { | |
| 388 | + | return fmt.Errorf("range not found in blacklist: %s", cidr) | |
| 389 | + | } | |
| 390 | + | ||
| 391 | + | // Update the list | |
| 392 | + | config.Ranges = newRanges | |
| 393 | + | ||
| 394 | + | // Write the updated config back to the file | |
| 395 | + | newData, err := yaml.Marshal(config) | |
| 396 | + | if err != nil { | |
| 397 | + | return err | |
| 398 | + | } | |
| 399 | + | ||
| 400 | + | if err := os.WriteFile(b.path, newData, 0644); err != nil { | |
| 401 | + | return err | |
| 402 | + | } | |
| 403 | + | ||
| 404 | + | log.Info("Removed CIDR range from blacklist", "cidr", cidr) | |
| 405 | + | ||
| 406 | + | // Force a reload to rebuild Networks and the cache | |
| 407 | + | return b.Load() | |
| 408 | + | } | |
| @@ -21,48 +21,65 @@ type BlacklistConfig struct { | |||||
| 21 | LastMod time.Time `yaml:"-"` // Last modification time | 21 | LastMod time.Time `yaml:"-"` // Last modification time | ||
| 22 | mu sync.RWMutex `yaml:"-"` // Mutex for thread safety | 22 | mu sync.RWMutex `yaml:"-"` // Mutex for thread safety | ||
| 23 | path string `yaml:"-"` // Path to blacklist file | 23 | path string `yaml:"-"` // Path to blacklist file | ||
| 24 | + | ||||
| 25 | + | // In-memory cache of blocked IPs for quick lookups | |||
| 26 | + | blockedCache map[string]bool `yaml:"-"` | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | // MarshalableConfig is a copy of BlacklistConfig without the mutex and other non-serializable fields | |||
| 30 | + | // Used to prevent copying mutex when marshaling to YAML | |||
| 31 | + | type MarshalableConfig struct { | |||
| 32 | + | IPs []string `yaml:"ips"` | |||
| 33 | + | Ranges []string `yaml:"ranges"` | |||
| 24 | } | 34 | } | ||
| 25 | 35 | ||||
| 26 | - | // NewBlacklist creates a new IP blacklist | 36 | + | // NewBlacklist creates a new BlacklistConfig from a file |
| 27 | func NewBlacklist(path string) (*BlacklistConfig, error) { | 37 | func NewBlacklist(path string) (*BlacklistConfig, error) { | ||
| 38 | + | // Create the blacklist config | |||
| 28 | blacklist := &BlacklistConfig{ | 39 | blacklist := &BlacklistConfig{ | ||
| 29 | - | path: path, | 40 | + | path: path, |
| 41 | + | blockedCache: make(map[string]bool), | |||
| 30 | } | 42 | } | ||
| 31 | 43 | ||||
| 32 | - | // Load initial blacklist | 44 | + | // Create the blacklist file if it doesn't exist |
| 33 | - | if err := blacklist.Load(); err != nil { | 45 | + | if _, err := os.Stat(path); os.IsNotExist(err) { |
| 34 | - | // If file doesn't exist, create an empty blacklist | 46 | + | // Create parent directory if it doesn't exist |
| 35 | - | if os.IsNotExist(err) { | 47 | + | dir := filepath.Dir(path) |
| 36 | - | log.Info("Blacklist file not found, creating empty blacklist", "path", path) | 48 | + | if _, err := os.Stat(dir); os.IsNotExist(err) { |
| 37 | - | blacklist.IPs = []string{} | |||
| 38 | - | blacklist.Ranges = []string{} | |||
| 39 | - | blacklist.Networks = []*net.IPNet{} | |||
| 40 | - | ||||
| 41 | - | // Create directory if needed | |||
| 42 | - | dir := filepath.Dir(path) | |||
| 43 | if err := os.MkdirAll(dir, 0755); err != nil { | 49 | if err := os.MkdirAll(dir, 0755); err != nil { | ||
| 44 | - | log.Error("Failed to create directory for blacklist", "dir", dir, "error", err) | 50 | + | return nil, fmt.Errorf("failed to create directory for blacklist: %w", err) |
| 45 | } | 51 | } | ||
| 52 | + | } | |||
| 46 | 53 | ||||
| 47 | - | // Write empty blacklist file | 54 | + | // Create the blacklist file with default empty structure |
| 48 | - | emptyConfig := &BlacklistConfig{ | 55 | + | data, err := yaml.Marshal(MarshalableConfig{ |
| 49 | - | IPs: []string{}, | 56 | + | IPs: []string{}, |
| 50 | - | Ranges: []string{}, | 57 | + | Ranges: []string{}, |
| 51 | - | } | 58 | + | }) |
| 52 | - | data, _ := yaml.Marshal(emptyConfig) | 59 | + | if err != nil { |
| 53 | - | if err := os.WriteFile(path, data, 0644); err != nil { | 60 | + | return nil, fmt.Errorf("failed to marshal default blacklist: %w", err) |
| 54 | - | log.Error("Failed to create empty blacklist file", "path", path, "error", err) | 61 | + | } |
| 55 | - | } | |||
| 56 | 62 | ||||
| 57 | - | return blacklist, nil | 63 | + | if err := os.WriteFile(path, data, 0644); err != nil { |
| 64 | + | return nil, fmt.Errorf("failed to create blacklist file: %w", err) | |||
| 58 | } | 65 | } | ||
| 59 | - | return nil, err | 66 | + | |
| 67 | + | log.Info("Created new blacklist file", "path", path) | |||
| 60 | } | 68 | } | ||
| 61 | 69 | ||||
| 70 | + | // Load the blacklist | |||
| 71 | + | if err := blacklist.Load(); err != nil { | |||
| 72 | + | return nil, fmt.Errorf("failed to load blacklist: %w", err) | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | log.Info("Blacklist loaded", | |||
| 76 | + | "ips", len(blacklist.IPs), | |||
| 77 | + | "ranges", len(blacklist.Ranges)) | |||
| 78 | + | ||||
| 62 | return blacklist, nil | 79 | return blacklist, nil | ||
| 63 | } | 80 | } | ||
| 64 | 81 | ||||
| 65 | - | // Load reads the blacklist from disk | 82 | + | // Load reloads the blacklist from the file if it has changed |
| 66 | func (b *BlacklistConfig) Load() error { | 83 | func (b *BlacklistConfig) Load() error { | ||
| 67 | b.mu.Lock() | 84 | b.mu.Lock() | ||
| 68 | defer b.mu.Unlock() | 85 | defer b.mu.Unlock() | ||
| @@ -87,7 +104,7 @@ func (b *BlacklistConfig) Load() error { | |||||
| 87 | } | 104 | } | ||
| 88 | 105 | ||||
| 89 | // Create a temporary config to unmarshal into | 106 | // Create a temporary config to unmarshal into | ||
| 90 | - | var tempConfig BlacklistConfig | 107 | + | var tempConfig MarshalableConfig |
| 91 | if err := yaml.Unmarshal(data, &tempConfig); err != nil { | 108 | if err := yaml.Unmarshal(data, &tempConfig); err != nil { | ||
| 92 | return err | 109 | return err | ||
| 93 | } | 110 | } | ||
| @@ -114,6 +131,9 @@ func (b *BlacklistConfig) Load() error { | |||||
| 114 | b.Networks = networks | 131 | b.Networks = networks | ||
| 115 | b.LastMod = fileInfo.ModTime() | 132 | b.LastMod = fileInfo.ModTime() | ||
| 116 | 133 | ||||
| 134 | + | // Rebuild the blocked cache | |||
| 135 | + | b.rebuildBlockedCache() | |||
| 136 | + | ||||
| 117 | // If the file has actually changed, log about it | 137 | // If the file has actually changed, log about it | ||
| 118 | if fileInfo.ModTime() != b.LastMod { | 138 | if fileInfo.ModTime() != b.LastMod { | ||
| 119 | log.Info("Loaded blacklist", | 139 | log.Info("Loaded blacklist", | ||
| @@ -129,6 +149,22 @@ func (b *BlacklistConfig) Load() error { | |||||
| 129 | return nil | 149 | return nil | ||
| 130 | } | 150 | } | ||
| 131 | 151 | ||||
| 152 | + | // rebuildBlockedCache rebuilds the in-memory cache of blocked IPs for quick lookups | |||
| 153 | + | func (b *BlacklistConfig) rebuildBlockedCache() { | |||
| 154 | + | // Create a new map to hold all blocked IPs | |||
| 155 | + | newCache := make(map[string]bool, len(b.IPs)) | |||
| 156 | + | ||||
| 157 | + | // Add direct IPs | |||
| 158 | + | for _, ip := range b.IPs { | |||
| 159 | + | newCache[ip] = true | |||
| 160 | + | } | |||
| 161 | + | ||||
| 162 | + | // Replace the cache atomically | |||
| 163 | + | b.blockedCache = newCache | |||
| 164 | + | ||||
| 165 | + | log.Debug("Rebuilt blocked IP cache", "direct_ips", len(b.blockedCache)) | |||
| 166 | + | } | |||
| 167 | + | ||||
| 132 | // IsBlocked checks if an IP is in the blacklist | 168 | // IsBlocked checks if an IP is in the blacklist | ||
| 133 | func (b *BlacklistConfig) IsBlocked(ip string) bool { | 169 | func (b *BlacklistConfig) IsBlocked(ip string) bool { | ||
| 134 | // Try to reload the blacklist first | 170 | // Try to reload the blacklist first | ||
| @@ -139,11 +175,9 @@ func (b *BlacklistConfig) IsBlocked(ip string) bool { | |||||
| 139 | b.mu.RLock() | 175 | b.mu.RLock() | ||
| 140 | defer b.mu.RUnlock() | 176 | defer b.mu.RUnlock() | ||
| 141 | 177 | ||||
| 142 | - | // Check direct IP match first (fastest) | 178 | + | // Fast path: Check direct IP match in cache first (fastest) |
| 143 | - | for _, blockedIP := range b.IPs { | 179 | + | if b.blockedCache[ip] { |
| 144 | - | if ip == blockedIP { | 180 | + | return true |
| 145 | - | return true | |||
| 146 | - | } | |||
| 147 | } | 181 | } | ||
| 148 | 182 | ||||
| 149 | // Parse the IP | 183 | // Parse the IP | ||
| @@ -156,6 +190,9 @@ func (b *BlacklistConfig) IsBlocked(ip string) bool { | |||||
| 156 | // Check if IP is in any blocked ranges | 190 | // Check if IP is in any blocked ranges | ||
| 157 | for _, network := range b.Networks { | 191 | for _, network := range b.Networks { | ||
| 158 | if network.Contains(parsedIP) { | 192 | if network.Contains(parsedIP) { | ||
| 193 | + | // Add to cache for faster lookups next time | |||
| 194 | + | // This is safe because we have a read lock and the map is only replaced, not modified | |||
| 195 | + | b.blockedCache[ip] = true | |||
| 159 | return true | 196 | return true | ||
| 160 | } | 197 | } | ||
| 161 | } | 198 | } | ||
| @@ -175,7 +212,7 @@ func (b *BlacklistConfig) AddIP(ip string) error { | |||||
| 175 | } | 212 | } | ||
| 176 | 213 | ||||
| 177 | // Parse the current data | 214 | // Parse the current data | ||
| 178 | - | var config BlacklistConfig | 215 | + | var config MarshalableConfig |
| 179 | if err := yaml.Unmarshal(data, &config); err != nil { | 216 | if err := yaml.Unmarshal(data, &config); err != nil { | ||
| 180 | return err | 217 | return err | ||
| 181 | } | 218 | } | ||
| @@ -200,21 +237,29 @@ func (b *BlacklistConfig) AddIP(ip string) error { | |||||
| 200 | return err | 237 | return err | ||
| 201 | } | 238 | } | ||
| 202 | 239 | ||||
| 240 | + | // Add to in-memory cache immediately | |||
| 241 | + | b.blockedCache[ip] = true | |||
| 242 | + | log.Info("Added IP to blacklist", "ip", ip) | |||
| 243 | + | ||||
| 203 | // Force a reload | 244 | // Force a reload | ||
| 204 | return b.Load() | 245 | return b.Load() | ||
| 205 | } | 246 | } | ||
| 206 | 247 | ||||
| 207 | // AddRange adds an IP range to the blacklist immediately | 248 | // AddRange adds an IP range to the blacklist immediately | ||
| 208 | func (b *BlacklistConfig) AddRange(cidr string) error { | 249 | func (b *BlacklistConfig) AddRange(cidr string) error { | ||
| 209 | - | // Make sure the CIDR is valid | 250 | + | b.mu.Lock() |
| 251 | + | defer b.mu.Unlock() | |||
| 252 | + | ||||
| 253 | + | // Validate the CIDR | |||
| 254 | + | if !strings.Contains(cidr, "/") { | |||
| 255 | + | cidr = cidr + "/32" // Add /32 for single IPs | |||
| 256 | + | } | |||
| 257 | + | ||||
| 210 | _, _, err := net.ParseCIDR(cidr) | 258 | _, _, err := net.ParseCIDR(cidr) | ||
| 211 | if err != nil { | 259 | if err != nil { | ||
| 212 | - | return fmt.Errorf("invalid CIDR: %w", err) | 260 | + | return fmt.Errorf("invalid CIDR format: %w", err) |
| 213 | } | 261 | } | ||
| 214 | 262 | ||||
| 215 | - | b.mu.Lock() | |||
| 216 | - | defer b.mu.Unlock() | |||
| 217 | - | ||||
| 218 | // Read the current file | 263 | // Read the current file | ||
| 219 | data, err := os.ReadFile(b.path) | 264 | data, err := os.ReadFile(b.path) | ||
| 220 | if err != nil { | 265 | if err != nil { | ||
| @@ -222,7 +267,7 @@ func (b *BlacklistConfig) AddRange(cidr string) error { | |||||
| 222 | } | 267 | } | ||
| 223 | 268 | ||||
| 224 | // Parse the current data | 269 | // Parse the current data | ||
| 225 | - | var config BlacklistConfig | 270 | + | var config MarshalableConfig |
| 226 | if err := yaml.Unmarshal(data, &config); err != nil { | 271 | if err := yaml.Unmarshal(data, &config); err != nil { | ||
| 227 | return err | 272 | return err | ||
| 228 | } | 273 | } | ||
| @@ -247,6 +292,117 @@ func (b *BlacklistConfig) AddRange(cidr string) error { | |||||
| 247 | return err | 292 | return err | ||
| 248 | } | 293 | } | ||
| 249 | 294 | ||||
| 295 | + | log.Info("Added CIDR range to blacklist", "cidr", cidr) | |||
| 296 | + | ||||
| 297 | + | // Force a reload to update Networks and the cache | |||
| 298 | + | return b.Load() | |||
| 299 | + | } | |||
| 300 | + | ||||
| 301 | + | // RemoveIP removes an IP from the blacklist | |||
| 302 | + | func (b *BlacklistConfig) RemoveIP(ip string) error { | |||
| 303 | + | b.mu.Lock() | |||
| 304 | + | defer b.mu.Unlock() | |||
| 305 | + | ||||
| 306 | + | // Read the current file | |||
| 307 | + | data, err := os.ReadFile(b.path) | |||
| 308 | + | if err != nil { | |||
| 309 | + | return err | |||
| 310 | + | } | |||
| 311 | + | ||||
| 312 | + | // Parse the current data | |||
| 313 | + | var config MarshalableConfig | |||
| 314 | + | if err := yaml.Unmarshal(data, &config); err != nil { | |||
| 315 | + | return err | |||
| 316 | + | } | |||
| 317 | + | ||||
| 318 | + | // Check if the IP is in the list | |||
| 319 | + | found := false | |||
| 320 | + | newIPs := make([]string, 0, len(config.IPs)) | |||
| 321 | + | for _, blockedIP := range config.IPs { | |||
| 322 | + | if blockedIP == ip { | |||
| 323 | + | found = true | |||
| 324 | + | continue // Skip this IP | |||
| 325 | + | } | |||
| 326 | + | newIPs = append(newIPs, blockedIP) | |||
| 327 | + | } | |||
| 328 | + | ||||
| 329 | + | if !found { | |||
| 330 | + | return fmt.Errorf("IP not found in blacklist: %s", ip) | |||
| 331 | + | } | |||
| 332 | + | ||||
| 333 | + | // Update the list | |||
| 334 | + | config.IPs = newIPs | |||
| 335 | + | ||||
| 336 | + | // Write the updated config back to the file | |||
| 337 | + | newData, err := yaml.Marshal(config) | |||
| 338 | + | if err != nil { | |||
| 339 | + | return err | |||
| 340 | + | } | |||
| 341 | + | ||||
| 342 | + | if err := os.WriteFile(b.path, newData, 0644); err != nil { | |||
| 343 | + | return err | |||
| 344 | + | } | |||
| 345 | + | ||||
| 346 | + | // Remove from cache | |||
| 347 | + | delete(b.blockedCache, ip) | |||
| 348 | + | log.Info("Removed IP from blacklist", "ip", ip) | |||
| 349 | + | ||||
| 250 | // Force a reload | 350 | // Force a reload | ||
| 251 | return b.Load() | 351 | return b.Load() | ||
| 252 | } | 352 | } | ||
| 353 | + | ||||
| 354 | + | // RemoveRange removes an IP range from the blacklist | |||
| 355 | + | func (b *BlacklistConfig) RemoveRange(cidr string) error { | |||
| 356 | + | b.mu.Lock() | |||
| 357 | + | defer b.mu.Unlock() | |||
| 358 | + | ||||
| 359 | + | // Normalize CIDR | |||
| 360 | + | if !strings.Contains(cidr, "/") { | |||
| 361 | + | cidr = cidr + "/32" // Add /32 for single IPs | |||
| 362 | + | } | |||
| 363 | + | ||||
| 364 | + | // Read the current file | |||
| 365 | + | data, err := os.ReadFile(b.path) | |||
| 366 | + | if err != nil { | |||
| 367 | + | return err | |||
| 368 | + | } | |||
| 369 | + | ||||
| 370 | + | // Parse the current data | |||
| 371 | + | var config MarshalableConfig | |||
| 372 | + | if err := yaml.Unmarshal(data, &config); err != nil { | |||
| 373 | + | return err | |||
| 374 | + | } | |||
| 375 | + | ||||
| 376 | + | // Check if the range is in the list | |||
| 377 | + | found := false | |||
| 378 | + | newRanges := make([]string, 0, len(config.Ranges)) | |||
| 379 | + | for _, blockedRange := range config.Ranges { | |||
| 380 | + | if blockedRange == cidr { | |||
| 381 | + | found = true | |||
| 382 | + | continue // Skip this range | |||
| 383 | + | } | |||
| 384 | + | newRanges = append(newRanges, blockedRange) | |||
| 385 | + | } | |||
| 386 | + | ||||
| 387 | + | if !found { | |||
| 388 | + | return fmt.Errorf("range not found in blacklist: %s", cidr) | |||
| 389 | + | } | |||
| 390 | + | ||||
| 391 | + | // Update the list | |||
| 392 | + | config.Ranges = newRanges | |||
| 393 | + | ||||
| 394 | + | // Write the updated config back to the file | |||
| 395 | + | newData, err := yaml.Marshal(config) | |||
| 396 | + | if err != nil { | |||
| 397 | + | return err | |||
| 398 | + | } | |||
| 399 | + | ||||
| 400 | + | if err := os.WriteFile(b.path, newData, 0644); err != nil { | |||
| 401 | + | return err | |||
| 402 | + | } | |||
| 403 | + | ||||
| 404 | + | log.Info("Removed CIDR range from blacklist", "cidr", cidr) | |||
| 405 | + | ||||
| 406 | + | // Force a reload to rebuild Networks and the cache | |||
| 407 | + | return b.Load() | |||
| 408 | + | } | |||
M internal/proxy/proxy.go +91 -72
| @@ -38,6 +38,10 @@ type Proxy struct { | |||
| 38 | 38 | lastBlockedLog time.Time | |
| 39 | 39 | blockedIPCounter map[string]int | |
| 40 | 40 | blockedIPCountMu sync.Mutex | |
| 41 | + | ||
| 42 | + | // Firewall-like memory of recently blocked IPs for quick rejection | |
| 43 | + | recentlyBlocked map[string]time.Time | |
| 44 | + | recentlyBlockedMu sync.RWMutex | |
| 41 | 45 | } | |
| 42 | 46 | ||
| 43 | 47 | // ProxyRouteInfo contains the information needed to route traffic to a container | |
| @@ -99,49 +103,11 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) { | |||
| 99 | 103 | httpsServer.HideBanner = true | |
| 100 | 104 | httpsServer.Use(middleware.Recover()) | |
| 101 | 105 | ||
| 102 | - | // Custom logger that skips blocked IPs | |
| 103 | - | httpsServer.Use(func(next echo.HandlerFunc) echo.HandlerFunc { | |
| 104 | - | return func(c echo.Context) error { | |
| 105 | - | // Store the context in the request context | |
| 106 | - | c.Set("reqContext", &requestContext{blacklisted: false}) | |
| 107 | - | return next(c) | |
| 108 | - | } | |
| 109 | - | }) | |
| 110 | - | ||
| 111 | - | httpsServer.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ | |
| 112 | - | Skipper: func(c echo.Context) bool { | |
| 113 | - | // Skip logging if the request is marked as blacklisted | |
| 114 | - | if reqCtx, ok := c.Get("reqContext").(*requestContext); ok && reqCtx.blacklisted { | |
| 115 | - | return true | |
| 116 | - | } | |
| 117 | - | return false | |
| 118 | - | }, | |
| 119 | - | })) | |
| 120 | - | ||
| 121 | 106 | // Create the HTTP echo instance (for redirects to HTTPS) | |
| 122 | 107 | httpServer := echo.New() | |
| 123 | 108 | httpServer.HideBanner = true | |
| 124 | 109 | httpServer.Use(middleware.Recover()) | |
| 125 | 110 | ||
| 126 | - | // Custom logger that skips blocked IPs | |
| 127 | - | httpServer.Use(func(next echo.HandlerFunc) echo.HandlerFunc { | |
| 128 | - | return func(c echo.Context) error { | |
| 129 | - | // Store the context in the request context | |
| 130 | - | c.Set("reqContext", &requestContext{blacklisted: false}) | |
| 131 | - | return next(c) | |
| 132 | - | } | |
| 133 | - | }) | |
| 134 | - | ||
| 135 | - | httpServer.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ | |
| 136 | - | Skipper: func(c echo.Context) bool { | |
| 137 | - | // Skip logging if the request is marked as blacklisted | |
| 138 | - | if reqCtx, ok := c.Get("reqContext").(*requestContext); ok && reqCtx.blacklisted { | |
| 139 | - | return true | |
| 140 | - | } | |
| 141 | - | return false | |
| 142 | - | }, | |
| 143 | - | })) | |
| 144 | - | ||
| 145 | 111 | // Create the proxy | |
| 146 | 112 | p := &Proxy{ | |
| 147 | 113 | config: config, | |
| @@ -153,8 +119,12 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) { | |||
| 153 | 119 | blacklist: blacklist, | |
| 154 | 120 | blockedIPCounter: make(map[string]int), | |
| 155 | 121 | lastBlockedLog: time.Time{}, // Zero time | |
| 122 | + | recentlyBlocked: make(map[string]time.Time), | |
| 156 | 123 | } | |
| 157 | 124 | ||
| 125 | + | // Now add the middleware with the proxy reference | |
| 126 | + | p.setupMiddleware() | |
| 127 | + | ||
| 158 | 128 | // Set up the certificate manager | |
| 159 | 129 | p.setupCertManager() | |
| 160 | 130 | ||
| @@ -162,6 +132,65 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) { | |||
| 162 | 132 | return p, nil | |
| 163 | 133 | } | |
| 164 | 134 | ||
| 135 | + | // setupMiddleware configures middleware for both HTTP and HTTPS servers | |
| 136 | + | func (p *Proxy) setupMiddleware() { | |
| 137 | + | // Create blacklist middleware for HTTPS | |
| 138 | + | p.httpsServer.Use(p.createBlacklistMiddleware()) | |
| 139 | + | ||
| 140 | + | // Add custom logger that skips blocked IPs | |
| 141 | + | p.httpsServer.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ | |
| 142 | + | Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}","status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}","bytes_in":${bytes_in},"bytes_out":${bytes_out}}`, | |
| 143 | + | Skipper: func(c echo.Context) bool { | |
| 144 | + | // Skip logging if the request is marked as blacklisted | |
| 145 | + | if reqCtx, ok := c.Get("reqContext").(*requestContext); ok && reqCtx.blacklisted { | |
| 146 | + | return true | |
| 147 | + | } | |
| 148 | + | return false | |
| 149 | + | }, | |
| 150 | + | })) | |
| 151 | + | ||
| 152 | + | // Create blacklist middleware for HTTP | |
| 153 | + | p.httpServer.Use(p.createBlacklistMiddleware()) | |
| 154 | + | ||
| 155 | + | // Add custom logger that skips blocked IPs | |
| 156 | + | p.httpServer.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ | |
| 157 | + | Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}","status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}","bytes_in":${bytes_in},"bytes_out":${bytes_out}}`, | |
| 158 | + | Skipper: func(c echo.Context) bool { | |
| 159 | + | // Skip logging if the request is marked as blacklisted | |
| 160 | + | if reqCtx, ok := c.Get("reqContext").(*requestContext); ok && reqCtx.blacklisted { | |
| 161 | + | return true | |
| 162 | + | } | |
| 163 | + | return false | |
| 164 | + | }, | |
| 165 | + | })) | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | // createBlacklistMiddleware returns a middleware function that checks IPs against the blacklist | |
| 169 | + | func (p *Proxy) createBlacklistMiddleware() echo.MiddlewareFunc { | |
| 170 | + | return func(next echo.HandlerFunc) echo.HandlerFunc { | |
| 171 | + | return func(c echo.Context) error { | |
| 172 | + | // Get client IP | |
| 173 | + | clientIP := c.RealIP() | |
| 174 | + | ||
| 175 | + | // Check if IP is blacklisted (if blacklist exists) | |
| 176 | + | if p.blacklist != nil && p.blacklist.IsBlocked(clientIP) { | |
| 177 | + | // Set context for other middleware | |
| 178 | + | c.Set("reqContext", &requestContext{blacklisted: true}) | |
| 179 | + | ||
| 180 | + | // Rate-limited logging of blocked IPs | |
| 181 | + | p.logBlockedIP(clientIP, c.Request().URL.Path, c.Request().UserAgent()) | |
| 182 | + | ||
| 183 | + | // Return forbidden without calling next handlers at all | |
| 184 | + | return c.String(http.StatusForbidden, "Forbidden") | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | // Store the context in the request context for non-blacklisted IPs | |
| 188 | + | c.Set("reqContext", &requestContext{blacklisted: false}) | |
| 189 | + | return next(c) | |
| 190 | + | } | |
| 191 | + | } | |
| 192 | + | } | |
| 193 | + | ||
| 165 | 194 | // setupCertManager configures the autocert manager for Let's Encrypt | |
| 166 | 195 | func (p *Proxy) setupCertManager() { | |
| 167 | 196 | // Create a cache directory if it doesn't exist | |
| @@ -402,19 +431,8 @@ func (p *Proxy) Start() error { | |||
| 402 | 431 | return nil | |
| 403 | 432 | } | |
| 404 | 433 | ||
| 405 | - | // Get client IP | |
| 406 | - | clientIP := c.RealIP() | |
| 407 | - | ||
| 408 | - | // Check if IP is blacklisted (if blacklist exists) | |
| 409 | - | if p.blacklist != nil && p.blacklist.IsBlocked(clientIP) { | |
| 410 | - | // Mark request as blacklisted to skip logging | |
| 411 | - | if reqCtx, ok := c.Get("reqContext").(*requestContext); ok { | |
| 412 | - | reqCtx.blacklisted = true | |
| 413 | - | } | |
| 414 | - | ||
| 415 | - | p.logBlockedIP(clientIP, c.Request().URL.Path, c.Request().UserAgent()) | |
| 416 | - | return c.String(http.StatusForbidden, "Forbidden") | |
| 417 | - | } | |
| 434 | + | // No need to check for blacklisting here as it's already checked in middleware | |
| 435 | + | // and all blacklisted requests will be stopped before reaching this handler | |
| 418 | 436 | ||
| 419 | 437 | host := c.Request().Host | |
| 420 | 438 | if strings.Contains(host, ":") { | |
| @@ -617,7 +635,7 @@ func (p *Proxy) loadRoutes() error { | |||
| 617 | 635 | return nil | |
| 618 | 636 | } | |
| 619 | 637 | ||
| 620 | - | // configureRoutes sets up the echo routes for proxying | |
| 638 | + | // configureRoutes sets up the HTTP and HTTPS routes | |
| 621 | 639 | func (p *Proxy) configureRoutes() { | |
| 622 | 640 | // Lock the routes map for reading | |
| 623 | 641 | p.mu.RLock() | |
| @@ -634,19 +652,8 @@ func (p *Proxy) configureRoutes() { | |||
| 634 | 652 | ||
| 635 | 653 | // Add a handler for all incoming requests (HTTPS) | |
| 636 | 654 | p.httpsServer.Any("/*", func(c echo.Context) error { | |
| 637 | - | // Get client IP | |
| 638 | - | clientIP := c.RealIP() | |
| 639 | - | ||
| 640 | - | // Check if IP is blacklisted (if blacklist exists) | |
| 641 | - | if p.blacklist != nil && p.blacklist.IsBlocked(clientIP) { | |
| 642 | - | // Mark request as blacklisted to skip logging | |
| 643 | - | if reqCtx, ok := c.Get("reqContext").(*requestContext); ok { | |
| 644 | - | reqCtx.blacklisted = true | |
| 645 | - | } | |
| 646 | - | ||
| 647 | - | p.logBlockedIP(clientIP, c.Request().URL.Path, c.Request().UserAgent()) | |
| 648 | - | return c.String(http.StatusForbidden, "Forbidden") | |
| 649 | - | } | |
| 655 | + | // No need to check for blacklisting here as it's already checked in middleware | |
| 656 | + | // and all blacklisted requests will be stopped before reaching this handler | |
| 650 | 657 | ||
| 651 | 658 | host := c.Request().Host | |
| 652 | 659 | ||
| @@ -919,16 +926,23 @@ func (p *Proxy) logBlockedIP(clientIP, path, userAgent string) { | |||
| 919 | 926 | ||
| 920 | 927 | now := time.Now() | |
| 921 | 928 | ||
| 922 | - | // If this is the first block or it's been more than 5 minutes since the last summary | |
| 923 | - | if p.lastBlockedLog.IsZero() || now.Sub(p.lastBlockedLog) > 5*time.Minute { | |
| 924 | - | // Log this block and reset counters | |
| 929 | + | // If this is the first block or it's been more than 5 minutes since the last summary, | |
| 930 | + | // or if this is a new IP we haven't seen recently | |
| 931 | + | if p.lastBlockedLog.IsZero() || | |
| 932 | + | now.Sub(p.lastBlockedLog) > 5*time.Minute || | |
| 933 | + | p.blockedIPCounter[clientIP] == 0 { | |
| 934 | + | ||
| 935 | + | // Log this block and update counters | |
| 925 | 936 | log.Info("Blocked request from blacklisted IP", | |
| 926 | 937 | "ip", clientIP, | |
| 927 | 938 | "path", path, | |
| 928 | 939 | "user_agent", userAgent) | |
| 929 | 940 | ||
| 930 | - | // Reset counters | |
| 931 | - | p.blockedIPCounter = make(map[string]int) | |
| 941 | + | // If we're resetting due to time, clear the counters | |
| 942 | + | if p.lastBlockedLog.IsZero() || now.Sub(p.lastBlockedLog) > 5*time.Minute { | |
| 943 | + | p.blockedIPCounter = make(map[string]int) | |
| 944 | + | } | |
| 945 | + | ||
| 932 | 946 | p.blockedIPCounter[clientIP] = 1 | |
| 933 | 947 | p.lastBlockedLog = now | |
| 934 | 948 | return | |
| @@ -937,8 +951,8 @@ func (p *Proxy) logBlockedIP(clientIP, path, userAgent string) { | |||
| 937 | 951 | // Increment counter for this IP | |
| 938 | 952 | p.blockedIPCounter[clientIP]++ | |
| 939 | 953 | ||
| 940 | - | // If it's been at least 60 seconds since the last log, print a summary | |
| 941 | - | if now.Sub(p.lastBlockedLog) >= 60*time.Second { | |
| 954 | + | // Only log summaries every 5 minutes by default | |
| 955 | + | if now.Sub(p.lastBlockedLog) >= 5*time.Minute { | |
| 942 | 956 | // Log summary of blocked requests | |
| 943 | 957 | totalBlocked := 0 | |
| 944 | 958 | for _, count := range p.blockedIPCounter { | |
| @@ -954,4 +968,9 @@ func (p *Proxy) logBlockedIP(clientIP, path, userAgent string) { | |||
| 954 | 968 | p.blockedIPCounter = make(map[string]int) | |
| 955 | 969 | p.lastBlockedLog = now | |
| 956 | 970 | } | |
| 971 | + | ||
| 972 | + | // Debug logging only if needed | |
| 973 | + | if false { | |
| 974 | + | log.Debug("Request marked as blacklisted for logging skip", "ip", clientIP) | |
| 975 | + | } | |
| 957 | 976 | } | |
| @@ -38,6 +38,10 @@ type Proxy struct { | |||||
| 38 | lastBlockedLog time.Time | 38 | lastBlockedLog time.Time | ||
| 39 | blockedIPCounter map[string]int | 39 | blockedIPCounter map[string]int | ||
| 40 | blockedIPCountMu sync.Mutex | 40 | blockedIPCountMu sync.Mutex | ||
| 41 | + | ||||
| 42 | + | // Firewall-like memory of recently blocked IPs for quick rejection | |||
| 43 | + | recentlyBlocked map[string]time.Time | |||
| 44 | + | recentlyBlockedMu sync.RWMutex | |||
| 41 | } | 45 | } | ||
| 42 | 46 | ||||
| 43 | // ProxyRouteInfo contains the information needed to route traffic to a container | 47 | // ProxyRouteInfo contains the information needed to route traffic to a container | ||
| @@ -99,49 +103,11 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) { | |||||
| 99 | httpsServer.HideBanner = true | 103 | httpsServer.HideBanner = true | ||
| 100 | httpsServer.Use(middleware.Recover()) | 104 | httpsServer.Use(middleware.Recover()) | ||
| 101 | 105 | ||||
| 102 | - | // Custom logger that skips blocked IPs | |||
| 103 | - | httpsServer.Use(func(next echo.HandlerFunc) echo.HandlerFunc { | |||
| 104 | - | return func(c echo.Context) error { | |||
| 105 | - | // Store the context in the request context | |||
| 106 | - | c.Set("reqContext", &requestContext{blacklisted: false}) | |||
| 107 | - | return next(c) | |||
| 108 | - | } | |||
| 109 | - | }) | |||
| 110 | - | ||||
| 111 | - | httpsServer.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ | |||
| 112 | - | Skipper: func(c echo.Context) bool { | |||
| 113 | - | // Skip logging if the request is marked as blacklisted | |||
| 114 | - | if reqCtx, ok := c.Get("reqContext").(*requestContext); ok && reqCtx.blacklisted { | |||
| 115 | - | return true | |||
| 116 | - | } | |||
| 117 | - | return false | |||
| 118 | - | }, | |||
| 119 | - | })) | |||
| 120 | - | ||||
| 121 | // Create the HTTP echo instance (for redirects to HTTPS) | 106 | // Create the HTTP echo instance (for redirects to HTTPS) | ||
| 122 | httpServer := echo.New() | 107 | httpServer := echo.New() | ||
| 123 | httpServer.HideBanner = true | 108 | httpServer.HideBanner = true | ||
| 124 | httpServer.Use(middleware.Recover()) | 109 | httpServer.Use(middleware.Recover()) | ||
| 125 | 110 | ||||
| 126 | - | // Custom logger that skips blocked IPs | |||
| 127 | - | httpServer.Use(func(next echo.HandlerFunc) echo.HandlerFunc { | |||
| 128 | - | return func(c echo.Context) error { | |||
| 129 | - | // Store the context in the request context | |||
| 130 | - | c.Set("reqContext", &requestContext{blacklisted: false}) | |||
| 131 | - | return next(c) | |||
| 132 | - | } | |||
| 133 | - | }) | |||
| 134 | - | ||||
| 135 | - | httpServer.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ | |||
| 136 | - | Skipper: func(c echo.Context) bool { | |||
| 137 | - | // Skip logging if the request is marked as blacklisted | |||
| 138 | - | if reqCtx, ok := c.Get("reqContext").(*requestContext); ok && reqCtx.blacklisted { | |||
| 139 | - | return true | |||
| 140 | - | } | |||
| 141 | - | return false | |||
| 142 | - | }, | |||
| 143 | - | })) | |||
| 144 | - | ||||
| 145 | // Create the proxy | 111 | // Create the proxy | ||
| 146 | p := &Proxy{ | 112 | p := &Proxy{ | ||
| 147 | config: config, | 113 | config: config, | ||
| @@ -153,8 +119,12 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) { | |||||
| 153 | blacklist: blacklist, | 119 | blacklist: blacklist, | ||
| 154 | blockedIPCounter: make(map[string]int), | 120 | blockedIPCounter: make(map[string]int), | ||
| 155 | lastBlockedLog: time.Time{}, // Zero time | 121 | lastBlockedLog: time.Time{}, // Zero time | ||
| 122 | + | recentlyBlocked: make(map[string]time.Time), | |||
| 156 | } | 123 | } | ||
| 157 | 124 | ||||
| 125 | + | // Now add the middleware with the proxy reference | |||
| 126 | + | p.setupMiddleware() | |||
| 127 | + | ||||
| 158 | // Set up the certificate manager | 128 | // Set up the certificate manager | ||
| 159 | p.setupCertManager() | 129 | p.setupCertManager() | ||
| 160 | 130 | ||||
| @@ -162,6 +132,65 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) { | |||||
| 162 | return p, nil | 132 | return p, nil | ||
| 163 | } | 133 | } | ||
| 164 | 134 | ||||
| 135 | + | // setupMiddleware configures middleware for both HTTP and HTTPS servers | |||
| 136 | + | func (p *Proxy) setupMiddleware() { | |||
| 137 | + | // Create blacklist middleware for HTTPS | |||
| 138 | + | p.httpsServer.Use(p.createBlacklistMiddleware()) | |||
| 139 | + | ||||
| 140 | + | // Add custom logger that skips blocked IPs | |||
| 141 | + | p.httpsServer.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ | |||
| 142 | + | Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}","status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}","bytes_in":${bytes_in},"bytes_out":${bytes_out}}`, | |||
| 143 | + | Skipper: func(c echo.Context) bool { | |||
| 144 | + | // Skip logging if the request is marked as blacklisted | |||
| 145 | + | if reqCtx, ok := c.Get("reqContext").(*requestContext); ok && reqCtx.blacklisted { | |||
| 146 | + | return true | |||
| 147 | + | } | |||
| 148 | + | return false | |||
| 149 | + | }, | |||
| 150 | + | })) | |||
| 151 | + | ||||
| 152 | + | // Create blacklist middleware for HTTP | |||
| 153 | + | p.httpServer.Use(p.createBlacklistMiddleware()) | |||
| 154 | + | ||||
| 155 | + | // Add custom logger that skips blocked IPs | |||
| 156 | + | p.httpServer.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ | |||
| 157 | + | Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}","status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}","bytes_in":${bytes_in},"bytes_out":${bytes_out}}`, | |||
| 158 | + | Skipper: func(c echo.Context) bool { | |||
| 159 | + | // Skip logging if the request is marked as blacklisted | |||
| 160 | + | if reqCtx, ok := c.Get("reqContext").(*requestContext); ok && reqCtx.blacklisted { | |||
| 161 | + | return true | |||
| 162 | + | } | |||
| 163 | + | return false | |||
| 164 | + | }, | |||
| 165 | + | })) | |||
| 166 | + | } | |||
| 167 | + | ||||
| 168 | + | // createBlacklistMiddleware returns a middleware function that checks IPs against the blacklist | |||
| 169 | + | func (p *Proxy) createBlacklistMiddleware() echo.MiddlewareFunc { | |||
| 170 | + | return func(next echo.HandlerFunc) echo.HandlerFunc { | |||
| 171 | + | return func(c echo.Context) error { | |||
| 172 | + | // Get client IP | |||
| 173 | + | clientIP := c.RealIP() | |||
| 174 | + | ||||
| 175 | + | // Check if IP is blacklisted (if blacklist exists) | |||
| 176 | + | if p.blacklist != nil && p.blacklist.IsBlocked(clientIP) { | |||
| 177 | + | // Set context for other middleware | |||
| 178 | + | c.Set("reqContext", &requestContext{blacklisted: true}) | |||
| 179 | + | ||||
| 180 | + | // Rate-limited logging of blocked IPs | |||
| 181 | + | p.logBlockedIP(clientIP, c.Request().URL.Path, c.Request().UserAgent()) | |||
| 182 | + | ||||
| 183 | + | // Return forbidden without calling next handlers at all | |||
| 184 | + | return c.String(http.StatusForbidden, "Forbidden") | |||
| 185 | + | } | |||
| 186 | + | ||||
| 187 | + | // Store the context in the request context for non-blacklisted IPs | |||
| 188 | + | c.Set("reqContext", &requestContext{blacklisted: false}) | |||
| 189 | + | return next(c) | |||
| 190 | + | } | |||
| 191 | + | } | |||
| 192 | + | } | |||
| 193 | + | ||||
| 165 | // setupCertManager configures the autocert manager for Let's Encrypt | 194 | // setupCertManager configures the autocert manager for Let's Encrypt | ||
| 166 | func (p *Proxy) setupCertManager() { | 195 | func (p *Proxy) setupCertManager() { | ||
| 167 | // Create a cache directory if it doesn't exist | 196 | // Create a cache directory if it doesn't exist | ||
| @@ -402,19 +431,8 @@ func (p *Proxy) Start() error { | |||||
| 402 | return nil | 431 | return nil | ||
| 403 | } | 432 | } | ||
| 404 | 433 | ||||
| 405 | - | // Get client IP | 434 | + | // No need to check for blacklisting here as it's already checked in middleware |
| 406 | - | clientIP := c.RealIP() | 435 | + | // and all blacklisted requests will be stopped before reaching this handler |
| 407 | - | ||||
| 408 | - | // Check if IP is blacklisted (if blacklist exists) | |||
| 409 | - | if p.blacklist != nil && p.blacklist.IsBlocked(clientIP) { | |||
| 410 | - | // Mark request as blacklisted to skip logging | |||
| 411 | - | if reqCtx, ok := c.Get("reqContext").(*requestContext); ok { | |||
| 412 | - | reqCtx.blacklisted = true | |||
| 413 | - | } | |||
| 414 | - | ||||
| 415 | - | p.logBlockedIP(clientIP, c.Request().URL.Path, c.Request().UserAgent()) | |||
| 416 | - | return c.String(http.StatusForbidden, "Forbidden") | |||
| 417 | - | } | |||
| 418 | 436 | ||||
| 419 | host := c.Request().Host | 437 | host := c.Request().Host | ||
| 420 | if strings.Contains(host, ":") { | 438 | if strings.Contains(host, ":") { | ||
| @@ -617,7 +635,7 @@ func (p *Proxy) loadRoutes() error { | |||||
| 617 | return nil | 635 | return nil | ||
| 618 | } | 636 | } | ||
| 619 | 637 | ||||
| 620 | - | // configureRoutes sets up the echo routes for proxying | 638 | + | // configureRoutes sets up the HTTP and HTTPS routes |
| 621 | func (p *Proxy) configureRoutes() { | 639 | func (p *Proxy) configureRoutes() { | ||
| 622 | // Lock the routes map for reading | 640 | // Lock the routes map for reading | ||
| 623 | p.mu.RLock() | 641 | p.mu.RLock() | ||
| @@ -634,19 +652,8 @@ func (p *Proxy) configureRoutes() { | |||||
| 634 | 652 | ||||
| 635 | // Add a handler for all incoming requests (HTTPS) | 653 | // Add a handler for all incoming requests (HTTPS) | ||
| 636 | p.httpsServer.Any("/*", func(c echo.Context) error { | 654 | p.httpsServer.Any("/*", func(c echo.Context) error { | ||
| 637 | - | // Get client IP | 655 | + | // No need to check for blacklisting here as it's already checked in middleware |
| 638 | - | clientIP := c.RealIP() | 656 | + | // and all blacklisted requests will be stopped before reaching this handler |
| 639 | - | ||||
| 640 | - | // Check if IP is blacklisted (if blacklist exists) | |||
| 641 | - | if p.blacklist != nil && p.blacklist.IsBlocked(clientIP) { | |||
| 642 | - | // Mark request as blacklisted to skip logging | |||
| 643 | - | if reqCtx, ok := c.Get("reqContext").(*requestContext); ok { | |||
| 644 | - | reqCtx.blacklisted = true | |||
| 645 | - | } | |||
| 646 | - | ||||
| 647 | - | p.logBlockedIP(clientIP, c.Request().URL.Path, c.Request().UserAgent()) | |||
| 648 | - | return c.String(http.StatusForbidden, "Forbidden") | |||
| 649 | - | } | |||
| 650 | 657 | ||||
| 651 | host := c.Request().Host | 658 | host := c.Request().Host | ||
| 652 | 659 | ||||
| @@ -919,16 +926,23 @@ func (p *Proxy) logBlockedIP(clientIP, path, userAgent string) { | |||||
| 919 | 926 | ||||
| 920 | now := time.Now() | 927 | now := time.Now() | ||
| 921 | 928 | ||||
| 922 | - | // If this is the first block or it's been more than 5 minutes since the last summary | 929 | + | // If this is the first block or it's been more than 5 minutes since the last summary, |
| 923 | - | if p.lastBlockedLog.IsZero() || now.Sub(p.lastBlockedLog) > 5*time.Minute { | 930 | + | // or if this is a new IP we haven't seen recently |
| 924 | - | // Log this block and reset counters | 931 | + | if p.lastBlockedLog.IsZero() || |
| 932 | + | now.Sub(p.lastBlockedLog) > 5*time.Minute || | |||
| 933 | + | p.blockedIPCounter[clientIP] == 0 { | |||
| 934 | + | ||||
| 935 | + | // Log this block and update counters | |||
| 925 | log.Info("Blocked request from blacklisted IP", | 936 | log.Info("Blocked request from blacklisted IP", | ||
| 926 | "ip", clientIP, | 937 | "ip", clientIP, | ||
| 927 | "path", path, | 938 | "path", path, | ||
| 928 | "user_agent", userAgent) | 939 | "user_agent", userAgent) | ||
| 929 | 940 | ||||
| 930 | - | // Reset counters | 941 | + | // If we're resetting due to time, clear the counters |
| 931 | - | p.blockedIPCounter = make(map[string]int) | 942 | + | if p.lastBlockedLog.IsZero() || now.Sub(p.lastBlockedLog) > 5*time.Minute { |
| 943 | + | p.blockedIPCounter = make(map[string]int) | |||
| 944 | + | } | |||
| 945 | + | ||||
| 932 | p.blockedIPCounter[clientIP] = 1 | 946 | p.blockedIPCounter[clientIP] = 1 | ||
| 933 | p.lastBlockedLog = now | 947 | p.lastBlockedLog = now | ||
| 934 | return | 948 | return | ||
| @@ -937,8 +951,8 @@ func (p *Proxy) logBlockedIP(clientIP, path, userAgent string) { | |||||
| 937 | // Increment counter for this IP | 951 | // Increment counter for this IP | ||
| 938 | p.blockedIPCounter[clientIP]++ | 952 | p.blockedIPCounter[clientIP]++ | ||
| 939 | 953 | ||||
| 940 | - | // If it's been at least 60 seconds since the last log, print a summary | 954 | + | // Only log summaries every 5 minutes by default |
| 941 | - | if now.Sub(p.lastBlockedLog) >= 60*time.Second { | 955 | + | if now.Sub(p.lastBlockedLog) >= 5*time.Minute { |
| 942 | // Log summary of blocked requests | 956 | // Log summary of blocked requests | ||
| 943 | totalBlocked := 0 | 957 | totalBlocked := 0 | ||
| 944 | for _, count := range p.blockedIPCounter { | 958 | for _, count := range p.blockedIPCounter { | ||
| @@ -954,4 +968,9 @@ func (p *Proxy) logBlockedIP(clientIP, path, userAgent string) { | |||||
| 954 | p.blockedIPCounter = make(map[string]int) | 968 | p.blockedIPCounter = make(map[string]int) | ||
| 955 | p.lastBlockedLog = now | 969 | p.lastBlockedLog = now | ||
| 956 | } | 970 | } | ||
| 971 | + | ||||
| 972 | + | // Debug logging only if needed | |||
| 973 | + | if false { | |||
| 974 | + | log.Debug("Request marked as blacklisted for logging skip", "ip", clientIP) | |||
| 975 | + | } | |||
| 957 | } | 976 | } | ||