Skip to content

Commit 9f7bd8a

Browse files
committed
file based list
1 parent 1ab2b98 commit 9f7bd8a

4 files changed

Lines changed: 305 additions & 108 deletions

File tree

.traefik.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ testData:
1313
- "192.168.0.0/16"
1414
- "10.0.0.0/8"
1515
- "127.0.0.0/8"
16-
SyncAllowlist: true
1716
AllowlistFile: "/etc/traefik/conf.d/allowlist.yml"
17+
PersistFile: "/etc/traefik/whitelist.json"

allowiprequest.go

Lines changed: 123 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package allowiprequest
22

33
import (
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

2425
func 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

158142
func (a *AllowIpR) serveSuccessPage(rw http.ResponseWriter, ip string) {
@@ -245,15 +229,10 @@ func (a *AllowIpR) serveAdminPage(rw http.ResponseWriter) {
245229
}
246230

247231
func (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

Comments
 (0)