@@ -2,6 +2,7 @@ package allowiprequest
22
33import (
44 "context"
5+ "encoding/json"
56 "fmt"
67 "log"
78 "net"
@@ -17,8 +18,8 @@ type Config struct {
1718 KnockURL string `json:"knockUrl,omitempty"`
1819 WhitelistDuration string `json:"whitelistDuration,omitempty"`
1920 AllowedSubnets []string `json:"allowedSubnets,omitempty"`
20- SyncAllowlist bool `json:"syncAllowlist,omitempty"`
2121 AllowlistFile string `json:"allowlistFile,omitempty"`
22+ PersistFile string `json:"persistFile,omitempty"`
2223}
2324
2425func CreateConfig () * Config {
@@ -36,8 +37,8 @@ type AllowIpR struct {
3637 whitelistDuration time.Duration
3738 allowedIPNets []* net.IPNet
3839 allowedSubnets []string
39- syncAllowlist bool
4040 allowlistFile string
41+ persistFile string
4142 whitelist map [string ]time.Time
4243 mu sync.RWMutex
4344}
@@ -68,16 +69,17 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
6869 whitelistDuration : duration ,
6970 allowedIPNets : allowedIPNets ,
7071 allowedSubnets : config .AllowedSubnets ,
71- syncAllowlist : config .SyncAllowlist ,
7272 allowlistFile : config .AllowlistFile ,
73+ persistFile : config .PersistFile ,
7374 whitelist : make (map [string ]time.Time ),
7475 }
7576
76- if d .syncAllowlist {
77- if d .allowlistFile == "" {
78- return nil , fmt .Errorf ("allowlistFile must be set when syncAllowlist is enabled" )
79- }
80- log .Printf ("[allowiprequest] allowlist sync enabled, file: %s" , d .allowlistFile )
77+ if d .persistFile != "" {
78+ d .loadWhitelist ()
79+ }
80+
81+ if d .allowlistFile != "" {
82+ log .Printf ("[allowiprequest] allowlist file: %s" , d .allowlistFile )
8183 d .writeAllowlist ()
8284 }
8385
@@ -118,9 +120,12 @@ func (a *AllowIpR) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
118120 a .mu .Lock ()
119121 a .whitelist [host ] = time .Now ().Add (a .whitelistDuration )
120122 a .mu .Unlock ()
121- if a .syncAllowlist {
123+ if a .allowlistFile != "" {
122124 a .writeAllowlist ()
123125 }
126+ if a .persistFile != "" {
127+ a .persistWhitelist ()
128+ }
124129 a .serveSuccessPage (rw , host )
125130 return
126131 }
@@ -131,28 +136,7 @@ func (a *AllowIpR) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
131136 return
132137 }
133138
134- // 4. Check Dynamic Whitelist
135- a .mu .RLock ()
136- expiry , exists := a .whitelist [host ]
137- a .mu .RUnlock ()
138-
139- if exists {
140- if time .Now ().Before (expiry ) {
141- a .next .ServeHTTP (rw , req )
142- return
143- } else {
144- // Clean up expired entry
145- a .mu .Lock ()
146- delete (a .whitelist , host )
147- a .mu .Unlock ()
148- if a .allowlistFile != "" {
149- a .writeAllowlist ()
150- }
151- }
152- }
153-
154- // Block by default
155- http .Error (rw , "Forbidden" , http .StatusForbidden )
139+ a .next .ServeHTTP (rw , req )
156140}
157141
158142func (a * AllowIpR ) serveSuccessPage (rw http.ResponseWriter , ip string ) {
@@ -245,15 +229,10 @@ func (a *AllowIpR) serveAdminPage(rw http.ResponseWriter) {
245229}
246230
247231func (a * AllowIpR ) writeAllowlist () {
248- var sb strings.Builder
249- sb .WriteString ("tcp:\n " )
250- sb .WriteString (" middlewares:\n " )
251- sb .WriteString (" local-whitelist:\n " )
252- sb .WriteString (" IPAllowList:\n " )
253- sb .WriteString (" sourceRange:\n " )
254-
232+ // Build the sourceRange entries shared by both tcp and http blocks
233+ var ranges strings.Builder
255234 for _ , subnet := range a .allowedSubnets {
256- sb .WriteString (fmt .Sprintf (" - \" %s\" \n " , subnet ))
235+ ranges .WriteString (fmt .Sprintf (" - \" %s\" \n " , subnet ))
257236 }
258237
259238 now := time .Now ()
@@ -265,14 +244,31 @@ func (a *AllowIpR) writeAllowlist() {
265244 continue
266245 }
267246 if parsed .To4 () != nil {
268- sb .WriteString (fmt .Sprintf (" - \" %s/32\" \n " , ip ))
247+ ranges .WriteString (fmt .Sprintf (" - \" %s/32\" \n " , ip ))
269248 } else {
270- sb .WriteString (fmt .Sprintf (" - \" %s/128\" \n " , ip ))
249+ ranges .WriteString (fmt .Sprintf (" - \" %s/128\" \n " , ip ))
271250 }
272251 }
273252 }
274253 a .mu .RUnlock ()
275254
255+ rangeStr := ranges .String ()
256+
257+ var sb strings.Builder
258+ sb .WriteString ("http:\n " )
259+ sb .WriteString (" middlewares:\n " )
260+ sb .WriteString (" local-whitelist-http:\n " )
261+ sb .WriteString (" IPAllowList:\n " )
262+ sb .WriteString (" sourceRange:\n " )
263+ sb .WriteString (rangeStr )
264+ sb .WriteString ("\n " )
265+ sb .WriteString ("tcp:\n " )
266+ sb .WriteString (" middlewares:\n " )
267+ sb .WriteString (" local-whitelist-tcp:\n " )
268+ sb .WriteString (" IPAllowList:\n " )
269+ sb .WriteString (" sourceRange:\n " )
270+ sb .WriteString (rangeStr )
271+
276272 dir := filepath .Dir (a .allowlistFile )
277273 if err := os .MkdirAll (dir , 0o755 ); err != nil {
278274 log .Printf ("[allowiprequest] failed to create directory %s: %v" , dir , err )
@@ -306,3 +302,89 @@ func (a *AllowIpR) writeAllowlist() {
306302
307303 log .Printf ("[allowiprequest] allowlist written to %s" , a .allowlistFile )
308304}
305+
306+ type persistEntry struct {
307+ IP string `json:"ip"`
308+ Expiry time.Time `json:"expiry"`
309+ }
310+
311+ func (a * AllowIpR ) persistWhitelist () {
312+ now := time .Now ()
313+ var entries []persistEntry
314+
315+ a .mu .RLock ()
316+ for ip , expiry := range a .whitelist {
317+ if now .Before (expiry ) {
318+ entries = append (entries , persistEntry {IP : ip , Expiry : expiry })
319+ }
320+ }
321+ a .mu .RUnlock ()
322+
323+ data , err := json .MarshalIndent (entries , "" , " " )
324+ if err != nil {
325+ log .Printf ("[allowiprequest] failed to marshal whitelist: %v" , err )
326+ return
327+ }
328+
329+ dir := filepath .Dir (a .persistFile )
330+ if err := os .MkdirAll (dir , 0o755 ); err != nil {
331+ log .Printf ("[allowiprequest] failed to create directory %s: %v" , dir , err )
332+ return
333+ }
334+
335+ tmp , err := os .CreateTemp (dir , ".persist-*.json.tmp" )
336+ if err != nil {
337+ log .Printf ("[allowiprequest] failed to create temp file in %s: %v" , dir , err )
338+ return
339+ }
340+ tmpName := tmp .Name ()
341+
342+ if _ , err := tmp .Write (data ); err != nil {
343+ tmp .Close ()
344+ os .Remove (tmpName )
345+ log .Printf ("[allowiprequest] failed to write persist temp file: %v" , err )
346+ return
347+ }
348+ if err := tmp .Close (); err != nil {
349+ os .Remove (tmpName )
350+ log .Printf ("[allowiprequest] failed to close persist temp file: %v" , err )
351+ return
352+ }
353+
354+ if err := os .Rename (tmpName , a .persistFile ); err != nil {
355+ os .Remove (tmpName )
356+ log .Printf ("[allowiprequest] failed to rename %s -> %s: %v" , tmpName , a .persistFile , err )
357+ return
358+ }
359+
360+ log .Printf ("[allowiprequest] whitelist persisted to %s" , a .persistFile )
361+ }
362+
363+ func (a * AllowIpR ) loadWhitelist () {
364+ data , err := os .ReadFile (a .persistFile )
365+ if err != nil {
366+ if os .IsNotExist (err ) {
367+ log .Printf ("[allowiprequest] no persist file found at %s, starting fresh" , a .persistFile )
368+ return
369+ }
370+ log .Printf ("[allowiprequest] failed to read persist file %s: %v" , a .persistFile , err )
371+ return
372+ }
373+
374+ var entries []persistEntry
375+ if err := json .Unmarshal (data , & entries ); err != nil {
376+ log .Printf ("[allowiprequest] failed to parse persist file %s: %v" , a .persistFile , err )
377+ return
378+ }
379+
380+ now := time .Now ()
381+ restored := 0
382+ for _ , e := range entries {
383+ if now .Before (e .Expiry ) {
384+ a .whitelist [e .IP ] = e .Expiry
385+ restored ++
386+ }
387+ }
388+
389+ log .Printf ("[allowiprequest] restored %d IPs from %s" , restored , a .persistFile )
390+ }
0 commit comments