Skip to content

Commit 52c0e3c

Browse files
committed
add more properties and start developing a CrowdSec integration
1 parent 142534e commit 52c0e3c

16 files changed

Lines changed: 1201 additions & 75 deletions

File tree

.gitattributes

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Normalize all text files to LF on commit
2+
* text=auto eol=lf
3+
4+
# Explicitly mark common text types
5+
*.go text eol=lf
6+
*.sh text eol=lf
7+
*.md text eol=lf
8+
*.json text eol=lf
9+
*.yaml text eol=lf
10+
*.yml text eol=lf
11+
*.txt text eol=lf
12+
*.mod text eol=lf
13+
*.sum text eol=lf
14+
*.toml text eol=lf
15+
*.svg text eol=lf
16+
*.xml text eol=lf
17+
18+
# Explicitly mark binary types (no line-ending conversion)
19+
*.png binary
20+
*.jpg binary
21+
*.jpeg binary
22+
*.gif binary
23+
*.ico binary
24+
*.exe binary

README.md

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,15 +206,44 @@ curl -X DELETE localhost:8090/block/1.2.3.4
206206
```
207207

208208

209+
## Blocking a Network (CIDR)
210+
211+
Add a network range to the blocklist:
212+
213+
```bash
214+
curl -X PUT localhost:8090/block-network/192.168.1.0/24
215+
```
216+
217+
Add with expiration and source:
218+
219+
```bash
220+
curl -X PUT "localhost:8090/block-network/10.0.0.0/8?duration=24h&source=manual"
221+
```
222+
223+
IPv6 networks are supported the same way:
224+
225+
```bash
226+
curl -X PUT "localhost:8090/block-network/2001:db8::/32?duration=12h"
227+
```
228+
229+
Remove a network block:
230+
231+
```bash
232+
curl -X DELETE localhost:8090/block-network/192.168.1.0/24
233+
```
234+
235+
The host bits of the supplied address are masked automatically, so `192.168.1.55/24` is stored as `192.168.1.0/24`.
236+
237+
209238
## HTTP Lookup API
210239

211-
Query the blocklist:
240+
Query the blocklist by IP address:
212241

213242
```bash
214243
curl localhost:8080/lookup/1.2.3.4
215244
```
216245

217-
Example response:
246+
Example response (exact IP match):
218247

219248
```json
220249
{
@@ -231,6 +260,19 @@ Example response:
231260
}
232261
```
233262

263+
If the IP is not individually blocked but falls within a blocked network range, the lookup still returns blocked and includes the matching CIDR:
264+
265+
```json
266+
{
267+
"blocked": true,
268+
"ip": "192.168.1.55",
269+
"matched_cidr": "192.168.1.0/24",
270+
"source": "manual",
271+
"remaining_duration": "23h59m12s",
272+
...
273+
}
274+
```
275+
234276

235277
## HTTP Metadata Headers
236278

@@ -251,6 +293,12 @@ X-Blocklist-Source: open-blocklist
251293
X-Blocklist-Remaining-Duration: permanent
252294
```
253295

296+
When the match is a CIDR network block, an additional header is returned:
297+
298+
```
299+
X-Blocklist-Matched-CIDR: 192.168.1.0/24
300+
```
301+
254302

255303
## Authorization Endpoint
256304

@@ -366,4 +414,19 @@ docker exec open-blocklist-etcd \
366414
etcdctl watch /skydns/internal/open-blocklist --prefix
367415
```
368416

417+
Network (CIDR) blocks are stored under a separate prefix:
418+
419+
```bash
420+
docker exec open-blocklist-etcd \
421+
etcdctl get /skydns/internal/open-blocklist-cidr --prefix
422+
```
423+
424+
Example:
425+
426+
```
427+
/skydns/internal/open-blocklist-cidr/192.168.1.0_24
428+
{"v":1,"host":"127.0.0.2","ttl":120,"source":"manual","first_seen":1773602647,"expiration":0}
429+
```
430+
```
431+
369432
This streams updates whenever entries are created, modified, or deleted.

container/dockerfile_wolfi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
FROM cgr.dev/chainguard/go:latest AS builder
44
ARG GIT_COMMIT
55
ARG GO_BUILD_TAGS
6-
WORKDIR /srv
6+
WORKDIR /src
77
COPY ./src .
88
RUN CGO_ENABLED=0 go build -tags "${GO_BUILD_TAGS}" -trimpath -ldflags="-s -w -X main.gBuildPlatform=wolfi" -o /tmp/open-blocklist
99

src/cidr.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// open-blocklist - CIDR table
2+
// Copyright Nash!Com, Daniel Nashed 2026 - APACHE 2.0 see LICENSE
3+
4+
package main
5+
6+
import (
7+
"fmt"
8+
"net"
9+
"sync"
10+
"time"
11+
)
12+
13+
// CIDREntry stores a blocked network range and its metadata.
14+
type CIDREntry struct {
15+
CIDR string // canonical CIDR, e.g. "1.2.3.0/24"
16+
Net *net.IPNet // parsed network for Contains() matching
17+
Source string
18+
Scenario string
19+
Action string
20+
ReturnCode uint32
21+
FirstSeen int64
22+
LastSeen int64
23+
Expiration int64
24+
}
25+
26+
type cidrTableStore struct {
27+
sync.RWMutex
28+
entries []*CIDREntry
29+
}
30+
31+
var cidrTable cidrTableStore
32+
33+
var (
34+
cidrPrefix = fmt.Sprintf("%s/internal/%s-cidr", SKY_DNS_PREFIX, RBL_ZONE)
35+
cidrPrefixScan = cidrPrefix + "/"
36+
)
37+
38+
// lookup returns the first CIDR entry that contains ip, or nil.
39+
// Sequential scan is fast enough for the expected scale (~2,000 entries).
40+
// Expired entries are skipped and queued for asynchronous deletion.
41+
func (t *cidrTableStore) lookup(ip net.IP) *CIDREntry {
42+
t.RLock()
43+
defer t.RUnlock()
44+
now := time.Now().Unix()
45+
for _, e := range t.entries {
46+
if e.Net.Contains(ip) {
47+
if e.Expiration > 0 && e.Expiration <= now {
48+
go deleteExpiredCIDR(e.CIDR)
49+
return nil
50+
}
51+
return e
52+
}
53+
}
54+
return nil
55+
}
56+
57+
func deleteExpiredCIDR(cidr string) {
58+
59+
etcdDeleteCIDR(cidr)
60+
61+
if !gMultiInstanceMode {
62+
cidrTable.remove(cidr)
63+
}
64+
65+
logMsg(LOG_INFO, "Lazy expiry: CIDR %s", cidr)
66+
}
67+
68+
// findByCIDR returns the entry with the exact canonical CIDR string, or nil.
69+
func (t *cidrTableStore) findByCIDR(cidr string) *CIDREntry {
70+
t.RLock()
71+
defer t.RUnlock()
72+
for _, e := range t.entries {
73+
if e.CIDR == cidr {
74+
return e
75+
}
76+
}
77+
return nil
78+
}
79+
80+
// upsert adds a new entry or replaces the existing one with the same CIDR.
81+
func (t *cidrTableStore) upsert(e *CIDREntry) {
82+
t.Lock()
83+
defer t.Unlock()
84+
for i, existing := range t.entries {
85+
if existing.CIDR == e.CIDR {
86+
t.entries[i] = e
87+
return
88+
}
89+
}
90+
t.entries = append(t.entries, e)
91+
}
92+
93+
// remove deletes the entry for the given CIDR using swap-with-last for O(1) removal.
94+
func (t *cidrTableStore) remove(cidr string) {
95+
t.Lock()
96+
defer t.Unlock()
97+
for i, e := range t.entries {
98+
if e.CIDR == cidr {
99+
last := len(t.entries) - 1
100+
t.entries[i] = t.entries[last]
101+
t.entries[last] = nil
102+
t.entries = t.entries[:last]
103+
return
104+
}
105+
}
106+
}
107+
108+
func (t *cidrTableStore) len() int {
109+
t.RLock()
110+
n := len(t.entries)
111+
t.RUnlock()
112+
return n
113+
}
114+
115+
// cidrEntryToMap returns a plain map suitable for JSON encoding.
116+
// net.IPNet is not included — only the canonical CIDR string is used.
117+
func cidrEntryToMap(e *CIDREntry) map[string]interface{} {
118+
return map[string]interface{}{
119+
"cidr": e.CIDR,
120+
"source": e.Source,
121+
"scenario": e.Scenario,
122+
"action": e.Action,
123+
124+
"first_seen": e.FirstSeen,
125+
"first_seen_iso": epochToISO(e.FirstSeen),
126+
127+
"last_seen": e.LastSeen,
128+
"last_seen_iso": epochToISO(e.LastSeen),
129+
130+
"expiration": e.Expiration,
131+
"expiration_iso": epochToISO(e.Expiration),
132+
133+
"remaining_seconds": remainingSeconds(e.Expiration),
134+
"remaining_duration": remainingDuration(e.Expiration),
135+
136+
"return_code": Uint32ToIPv4Str(e.ReturnCode),
137+
}
138+
}
139+
140+
// cidrEntryToBlockEntry converts a CIDREntry for use in HTTP responses.
141+
// The caller is responsible for overriding IP with the queried IP address.
142+
func cidrEntryToBlockEntry(e *CIDREntry) *BlockEntry {
143+
return &BlockEntry{
144+
IP: e.CIDR,
145+
Source: e.Source,
146+
Scenario: e.Scenario,
147+
Action: e.Action,
148+
ReturnCode: e.ReturnCode,
149+
FirstSeen: e.FirstSeen,
150+
LastSeen: e.LastSeen,
151+
Expiration: e.Expiration,
152+
}
153+
}

0 commit comments

Comments
 (0)