detached 5a8e006

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 (
8080 gracePeriod = 30 // seconds
8181 )
8282
83+// Track whether config logs have been printed already
84+var configLogsPrinted bool = false
85+
8386 func getConfigDir() (string, error) {
8487 var configDir string
8588
@@ -139,116 +142,162 @@ func readAndUnmarshalConfig(fs fs.FS, filePath string, config *Config) error {
139142 }
140143
141144 // loadConfigFromEnv loads configuration from environment variables
142-func loadConfigFromEnv(config *Config) {
145+func loadConfigFromEnv(config *Config, printLogs bool) {
143146 // General Configuration
144147 if val := os.Getenv("GORDON_STORAGE_DIR"); val != "" {
145148 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+ }
147152 }
148153 if val := os.Getenv("GORDON_TOKEN"); val != "" {
149154 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+ }
151158 }
152159
153160 // HTTP Configuration
154161 if val := os.Getenv("GORDON_HTTP_PORT"); val != "" {
155162 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+ }
157166 }
158167 if val := os.Getenv("GORDON_HTTP_DOMAIN"); val != "" {
159168 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+ }
161172 }
162173 if val := os.Getenv("GORDON_HTTP_SUBDOMAIN"); val != "" {
163174 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+ }
165178 }
166179 if val := os.Getenv("GORDON_HTTP_BACKEND_URL"); val != "" {
167180 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+ }
169184 }
170185 if val := os.Getenv("GORDON_HTTP_HTTPS"); val != "" {
171186 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+ }
173190 }
174191
175192 // Admin Configuration
176193 if val := os.Getenv("GORDON_ADMIN_PATH"); val != "" {
177194 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+ }
179198 }
180199
181200 // Container Engine Configuration
182201 if val := os.Getenv("GORDON_CONTAINER_SOCK"); val != "" {
183202 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+ }
185206 }
186207 if val := os.Getenv("GORDON_CONTAINER_PODMAN_SOCK"); val != "" {
187208 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+ }
189212 }
190213 if val := os.Getenv("GORDON_CONTAINER_PODMAN"); val != "" {
191214 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+ }
193218
194219 // If using Podman, and Sock is empty but PodmanSock is set, use PodmanSock for Sock field
195220 if config.ContainerEngine.Podman && config.ContainerEngine.Sock == "" && config.ContainerEngine.PodmanSock != "" {
196221 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+ }
198225 }
199226 }
200227 if val := os.Getenv("GORDON_CONTAINER_NETWORK"); val != "" {
201228 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+ }
203232 }
204233
205234 // If using Docker socket with podman-compose mounting
206235 if config.ContainerEngine.Sock == "" && fileExists("/var/run/docker.sock") {
207236 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+ }
209240 }
210241
211242 // Reverse Proxy Configuration
212243 if val := os.Getenv("GORDON_PROXY_PORT"); val != "" {
213244 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+ }
215248 }
216249 if val := os.Getenv("GORDON_PROXY_HTTP_PORT"); val != "" {
217250 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+ }
219254 }
220255 if val := os.Getenv("GORDON_PROXY_CERT_DIR"); val != "" {
221256 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+ }
223260 }
224261 if val := os.Getenv("GORDON_PROXY_AUTO_RENEW"); val != "" {
225262 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+ }
227266 }
228267 if val := os.Getenv("GORDON_PROXY_RENEW_BEFORE"); val != "" {
229268 if i, err := strconv.Atoi(val); err == nil {
230269 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+ }
232273 }
233274 }
234275 if val := os.Getenv("GORDON_PROXY_LETSENCRYPT_MODE"); val != "" {
235276 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+ }
237280 }
238281 if val := os.Getenv("GORDON_PROXY_EMAIL"); val != "" {
239282 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+ }
241286 }
242287 if val := os.Getenv("GORDON_PROXY_CACHE_SIZE"); val != "" {
243288 if i, err := strconv.Atoi(val); err == nil {
244289 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+ }
246293 }
247294 }
248295 if val := os.Getenv("GORDON_PROXY_GRACE_PERIOD"); val != "" {
249296 if i, err := strconv.Atoi(val); err == nil {
250297 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+ }
252301 }
253302 }
254303 }
@@ -268,6 +317,11 @@ func (config *Config) LoadConfig() (*Config, error) {
268317 configFilePath = filepath.Join(configDir, "config.yml")
269318 }
270319
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+
271325 configExists := true
272326 _, err = os.Stat(configFilePath)
273327 if errors.Is(err, fs.ErrNotExist) {
@@ -339,8 +393,11 @@ func (config *Config) LoadConfig() (*Config, error) {
339393
340394 // Only load environment variables once, after either creating a new config or reading an existing one
341395 if docker.IsRunningInContainer() {
396+ // Only print logs if we haven't printed them before
397+ shouldPrintLogs := !configLogsPrinted
342398 fmt.Println("Running in container, overriding configuration with environment variables...")
343- loadConfigFromEnv(config)
399+ loadConfigFromEnv(config, shouldPrintLogs)
400+ configLogsPrinted = true
344401 }
345402
346403 // Load run env
@@ -359,12 +416,18 @@ func (config *Config) LoadConfig() (*Config, error) {
359416 }
360417
361418 // 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+ }
368431
369432 return config, nil
370433 }
@@ -80,6 +80,9 @@ var (
80 gracePeriod = 30 // seconds80 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 string87 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 variables144 // loadConfigFromEnv loads configuration from environment variables
142-func loadConfigFromEnv(config *Config) {145+func loadConfigFromEnv(config *Config, printLogs bool) {
143 // General Configuration146 // General Configuration
144 if val := os.Getenv("GORDON_STORAGE_DIR"); val != "" {147 if val := os.Getenv("GORDON_STORAGE_DIR"); val != "" {
145 config.General.StorageDir = val148 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 = val154 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 Configuration160 // HTTP Configuration
154 if val := os.Getenv("GORDON_HTTP_PORT"); val != "" {161 if val := os.Getenv("GORDON_HTTP_PORT"); val != "" {
155 config.Http.Port = val162 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 = val168 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 = val174 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 = val180 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 Configuration192 // Admin Configuration
176 if val := os.Getenv("GORDON_ADMIN_PATH"); val != "" {193 if val := os.Getenv("GORDON_ADMIN_PATH"); val != "" {
177 config.Admin.Path = val194 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 Configuration200 // 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 = val202 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 = val208 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 field219 // 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.PodmanSock221 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 = val228 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 mounting234 // 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 Configuration242 // 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 = val244 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 = val250 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 = val256 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 = i269 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 = val276 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 = val282 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 = i289 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 = i297 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 := true325 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 one394 // 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 env403 // 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 correctly418 // 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, nil432 return config, nil
370 }433 }
M internal/proxy/blacklist.go +195 -39
@@ -21,48 +21,65 @@ type BlacklistConfig struct {
2121 LastMod time.Time `yaml:"-"` // Last modification time
2222 mu sync.RWMutex `yaml:"-"` // Mutex for thread safety
2323 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"`
2434 }
2535
26-// NewBlacklist creates a new IP blacklist
36+// NewBlacklist creates a new BlacklistConfig from a file
2737 func NewBlacklist(path string) (*BlacklistConfig, error) {
38+ // Create the blacklist config
2839 blacklist := &BlacklistConfig{
29- path: path,
40+ path: path,
41+ blockedCache: make(map[string]bool),
3042 }
3143
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) {
4349 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)
4551 }
52+ }
4653
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+ }
5662
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)
5865 }
59- return nil, err
66+
67+ log.Info("Created new blacklist file", "path", path)
6068 }
6169
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+
6279 return blacklist, nil
6380 }
6481
65-// Load reads the blacklist from disk
82+// Load reloads the blacklist from the file if it has changed
6683 func (b *BlacklistConfig) Load() error {
6784 b.mu.Lock()
6885 defer b.mu.Unlock()
@@ -87,7 +104,7 @@ func (b *BlacklistConfig) Load() error {
87104 }
88105
89106 // Create a temporary config to unmarshal into
90- var tempConfig BlacklistConfig
107+ var tempConfig MarshalableConfig
91108 if err := yaml.Unmarshal(data, &tempConfig); err != nil {
92109 return err
93110 }
@@ -114,6 +131,9 @@ func (b *BlacklistConfig) Load() error {
114131 b.Networks = networks
115132 b.LastMod = fileInfo.ModTime()
116133
134+ // Rebuild the blocked cache
135+ b.rebuildBlockedCache()
136+
117137 // If the file has actually changed, log about it
118138 if fileInfo.ModTime() != b.LastMod {
119139 log.Info("Loaded blacklist",
@@ -129,6 +149,22 @@ func (b *BlacklistConfig) Load() error {
129149 return nil
130150 }
131151
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+
132168 // IsBlocked checks if an IP is in the blacklist
133169 func (b *BlacklistConfig) IsBlocked(ip string) bool {
134170 // Try to reload the blacklist first
@@ -139,11 +175,9 @@ func (b *BlacklistConfig) IsBlocked(ip string) bool {
139175 b.mu.RLock()
140176 defer b.mu.RUnlock()
141177
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
147181 }
148182
149183 // Parse the IP
@@ -156,6 +190,9 @@ func (b *BlacklistConfig) IsBlocked(ip string) bool {
156190 // Check if IP is in any blocked ranges
157191 for _, network := range b.Networks {
158192 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
159196 return true
160197 }
161198 }
@@ -175,7 +212,7 @@ func (b *BlacklistConfig) AddIP(ip string) error {
175212 }
176213
177214 // Parse the current data
178- var config BlacklistConfig
215+ var config MarshalableConfig
179216 if err := yaml.Unmarshal(data, &config); err != nil {
180217 return err
181218 }
@@ -200,21 +237,29 @@ func (b *BlacklistConfig) AddIP(ip string) error {
200237 return err
201238 }
202239
240+ // Add to in-memory cache immediately
241+ b.blockedCache[ip] = true
242+ log.Info("Added IP to blacklist", "ip", ip)
243+
203244 // Force a reload
204245 return b.Load()
205246 }
206247
207248 // AddRange adds an IP range to the blacklist immediately
208249 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+
210258 _, _, err := net.ParseCIDR(cidr)
211259 if err != nil {
212- return fmt.Errorf("invalid CIDR: %w", err)
260+ return fmt.Errorf("invalid CIDR format: %w", err)
213261 }
214262
215- b.mu.Lock()
216- defer b.mu.Unlock()
217-
218263 // Read the current file
219264 data, err := os.ReadFile(b.path)
220265 if err != nil {
@@ -222,7 +267,7 @@ func (b *BlacklistConfig) AddRange(cidr string) error {
222267 }
223268
224269 // Parse the current data
225- var config BlacklistConfig
270+ var config MarshalableConfig
226271 if err := yaml.Unmarshal(data, &config); err != nil {
227272 return err
228273 }
@@ -247,6 +292,117 @@ func (b *BlacklistConfig) AddRange(cidr string) error {
247292 return err
248293 }
249294
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+
250350 // Force a reload
251351 return b.Load()
252352 }
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 time21 LastMod time.Time `yaml:"-"` // Last modification time
22 mu sync.RWMutex `yaml:"-"` // Mutex for thread safety22 mu sync.RWMutex `yaml:"-"` // Mutex for thread safety
23 path string `yaml:"-"` // Path to blacklist file23 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 blacklist36+// 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 blacklist44+ // 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 blacklist46+ // 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 file54+ // 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, nil63+ 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, err66+
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, nil79 return blacklist, nil
63 }80 }
64 81
65-// Load reads the blacklist from disk82+// 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 into106 // Create a temporary config to unmarshal into
90- var tempConfig BlacklistConfig107+ var tempConfig MarshalableConfig
91 if err := yaml.Unmarshal(data, &tempConfig); err != nil {108 if err := yaml.Unmarshal(data, &tempConfig); err != nil {
92 return err109 return err
93 }110 }
@@ -114,6 +131,9 @@ func (b *BlacklistConfig) Load() error {
114 b.Networks = networks131 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 it137 // 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 nil149 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 blacklist168 // 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 first170 // 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 IP183 // Parse the IP
@@ -156,6 +190,9 @@ func (b *BlacklistConfig) IsBlocked(ip string) bool {
156 // Check if IP is in any blocked ranges190 // 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 true196 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 data214 // Parse the current data
178- var config BlacklistConfig215+ var config MarshalableConfig
179 if err := yaml.Unmarshal(data, &config); err != nil {216 if err := yaml.Unmarshal(data, &config); err != nil {
180 return err217 return err
181 }218 }
@@ -200,21 +237,29 @@ func (b *BlacklistConfig) AddIP(ip string) error {
200 return err237 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 reload244 // Force a reload
204 return b.Load()245 return b.Load()
205 }246 }
206 247
207 // AddRange adds an IP range to the blacklist immediately248 // 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 valid250+ 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 file263 // 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 data269 // Parse the current data
225- var config BlacklistConfig270+ var config MarshalableConfig
226 if err := yaml.Unmarshal(data, &config); err != nil {271 if err := yaml.Unmarshal(data, &config); err != nil {
227 return err272 return err
228 }273 }
@@ -247,6 +292,117 @@ func (b *BlacklistConfig) AddRange(cidr string) error {
247 return err292 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 reload350 // 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 {
3838 lastBlockedLog time.Time
3939 blockedIPCounter map[string]int
4040 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
4145 }
4246
4347 // ProxyRouteInfo contains the information needed to route traffic to a container
@@ -99,49 +103,11 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) {
99103 httpsServer.HideBanner = true
100104 httpsServer.Use(middleware.Recover())
101105
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-
121106 // Create the HTTP echo instance (for redirects to HTTPS)
122107 httpServer := echo.New()
123108 httpServer.HideBanner = true
124109 httpServer.Use(middleware.Recover())
125110
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-
145111 // Create the proxy
146112 p := &Proxy{
147113 config: config,
@@ -153,8 +119,12 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) {
153119 blacklist: blacklist,
154120 blockedIPCounter: make(map[string]int),
155121 lastBlockedLog: time.Time{}, // Zero time
122+ recentlyBlocked: make(map[string]time.Time),
156123 }
157124
125+ // Now add the middleware with the proxy reference
126+ p.setupMiddleware()
127+
158128 // Set up the certificate manager
159129 p.setupCertManager()
160130
@@ -162,6 +132,65 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) {
162132 return p, nil
163133 }
164134
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+
165194 // setupCertManager configures the autocert manager for Let's Encrypt
166195 func (p *Proxy) setupCertManager() {
167196 // Create a cache directory if it doesn't exist
@@ -402,19 +431,8 @@ func (p *Proxy) Start() error {
402431 return nil
403432 }
404433
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
418436
419437 host := c.Request().Host
420438 if strings.Contains(host, ":") {
@@ -617,7 +635,7 @@ func (p *Proxy) loadRoutes() error {
617635 return nil
618636 }
619637
620-// configureRoutes sets up the echo routes for proxying
638+// configureRoutes sets up the HTTP and HTTPS routes
621639 func (p *Proxy) configureRoutes() {
622640 // Lock the routes map for reading
623641 p.mu.RLock()
@@ -634,19 +652,8 @@ func (p *Proxy) configureRoutes() {
634652
635653 // Add a handler for all incoming requests (HTTPS)
636654 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
650657
651658 host := c.Request().Host
652659
@@ -919,16 +926,23 @@ func (p *Proxy) logBlockedIP(clientIP, path, userAgent string) {
919926
920927 now := time.Now()
921928
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
925936 log.Info("Blocked request from blacklisted IP",
926937 "ip", clientIP,
927938 "path", path,
928939 "user_agent", userAgent)
929940
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+
932946 p.blockedIPCounter[clientIP] = 1
933947 p.lastBlockedLog = now
934948 return
@@ -937,8 +951,8 @@ func (p *Proxy) logBlockedIP(clientIP, path, userAgent string) {
937951 // Increment counter for this IP
938952 p.blockedIPCounter[clientIP]++
939953
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 {
942956 // Log summary of blocked requests
943957 totalBlocked := 0
944958 for _, count := range p.blockedIPCounter {
@@ -954,4 +968,9 @@ func (p *Proxy) logBlockedIP(clientIP, path, userAgent string) {
954968 p.blockedIPCounter = make(map[string]int)
955969 p.lastBlockedLog = now
956970 }
971+
972+ // Debug logging only if needed
973+ if false {
974+ log.Debug("Request marked as blacklisted for logging skip", "ip", clientIP)
975+ }
957976 }
@@ -38,6 +38,10 @@ type Proxy struct {
38 lastBlockedLog time.Time38 lastBlockedLog time.Time
39 blockedIPCounter map[string]int39 blockedIPCounter map[string]int
40 blockedIPCountMu sync.Mutex40 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 container47 // 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 = true103 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 = true108 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 proxy111 // 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 time121 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 manager128 // 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, nil132 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 Encrypt194 // 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 exist196 // Create a cache directory if it doesn't exist
@@ -402,19 +431,8 @@ func (p *Proxy) Start() error {
402 return nil431 return nil
403 }432 }
404 433
405- // Get client IP434+ // 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().Host437 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 nil635 return nil
618 }636 }
619 637
620-// configureRoutes sets up the echo routes for proxying638+// configureRoutes sets up the HTTP and HTTPS routes
621 func (p *Proxy) configureRoutes() {639 func (p *Proxy) configureRoutes() {
622 // Lock the routes map for reading640 // 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 IP655+ // 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().Host658 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 summary929+ // 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 counters931+ 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 counters941+ // 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] = 1946 p.blockedIPCounter[clientIP] = 1
933 p.lastBlockedLog = now947 p.lastBlockedLog = now
934 return948 return
@@ -937,8 +951,8 @@ func (p *Proxy) logBlockedIP(clientIP, path, userAgent string) {
937 // Increment counter for this IP951 // 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 summary954+ // 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 requests956 // Log summary of blocked requests
943 totalBlocked := 0957 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 = now969 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 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close