Compare commits

...

33 Commits

Author SHA1 Message Date
a9d62c0c5d bugfix metrics
All checks were successful
release-tag / release-image (push) Successful in 2m3s
2025-11-13 13:31:11 +01:00
2eea551964 Check-Gui added
All checks were successful
release-tag / release-image (push) Successful in 1m49s
2025-11-12 03:33:16 +01:00
2cf8c4204d fix2
All checks were successful
release-tag / release-image (push) Successful in 1m58s
2025-11-10 21:59:25 +01:00
6898fdd47e fix
All checks were successful
release-tag / release-image (push) Successful in 2m6s
2025-11-10 21:53:49 +01:00
59556eae01 Metrics angepasst (Test)
All checks were successful
release-tag / release-image (push) Successful in 3m32s
2025-11-10 21:44:03 +01:00
5b48c30a98 updates
All checks were successful
release-tag / release-image (push) Successful in 3m29s
2025-11-10 21:24:16 +01:00
19143fd8c1 updated template
All checks were successful
release-tag / release-image (push) Successful in 1m47s
2025-11-09 15:36:01 +01:00
6d326ba495 updated layout
All checks were successful
release-tag / release-image (push) Successful in 1m49s
2025-11-09 15:31:46 +01:00
c7e9625236 fix
All checks were successful
release-tag / release-image (push) Successful in 2m0s
2025-11-08 23:51:24 +01:00
2a66ea48e8 updated to unauthorized
All checks were successful
release-tag / release-image (push) Successful in 1m52s
2025-11-08 23:46:14 +01:00
eff3dd61af changed
All checks were successful
release-tag / release-image (push) Successful in 1m46s
2025-11-08 22:54:28 +01:00
840ecf2953 test with blockpage 2025-11-08 22:54:16 +01:00
c5a7f90226 update
All checks were successful
release-tag / release-image (push) Successful in 1m52s
2025-11-08 21:21:22 +01:00
8a4ec32861 bugfix3
All checks were successful
release-tag / release-image (push) Successful in 1m46s
2025-06-21 22:42:43 +02:00
9cb11db43a bugfix2
All checks were successful
release-tag / release-image (push) Successful in 1m38s
2025-06-21 22:36:37 +02:00
5d8c3c313c bugfixes
All checks were successful
release-tag / release-image (push) Successful in 2m14s
2025-06-21 22:12:16 +02:00
6900c4dd75 Make
All checks were successful
release-tag / release-image (push) Successful in 1m41s
2025-06-18 21:06:50 +02:00
43a088d118 Merge branch 'main' of https://git.send.nrw/sendnrw/flod
Some checks failed
release-tag / release-image (push) Has been cancelled
2025-06-18 21:06:05 +02:00
7518619a74 Added ENV for Import-URL 2025-06-18 21:05:40 +02:00
95e14caeb6 main.go aktualisiert
All checks were successful
release-tag / release-image (push) Successful in 2m0s
2025-06-18 08:45:53 +00:00
b87c8a9a6d Rollback without Ranger
All checks were successful
release-tag / release-image (push) Successful in 1m41s
2025-06-17 18:18:16 +02:00
3e57aaa098 RC-0.3.0 (3)
All checks were successful
release-tag / release-image (push) Successful in 2m1s
2025-06-17 18:07:38 +02:00
e074327e49 RC-0.3.0 (2)
All checks were successful
release-tag / release-image (push) Successful in 2m9s
2025-06-17 17:40:07 +02:00
01b0b8228e RC-0.3.0 (1) 2025-06-17 17:26:21 +02:00
753893c836 Release 0.2.0
All checks were successful
release-tag / release-image (push) Successful in 1m40s
2025-06-17 00:19:45 +02:00
7265a37877 fast-fix
All checks were successful
release-tag / release-image (push) Successful in 1m39s
2025-06-16 23:28:13 +02:00
d5346b75dc Bugfix folder not found
All checks were successful
release-tag / release-image (push) Successful in 1m40s
2025-06-16 22:47:16 +02:00
d53767ae3c Updated Stack to support Importer
All checks were successful
release-tag / release-image (push) Successful in 1m42s
2025-06-16 22:28:42 +02:00
114f4ac7dc Heavy Increase Number of Blocklists from IPv64.net
All checks were successful
release-tag / release-image (push) Successful in 1m40s
2025-06-16 17:22:17 +02:00
ce5de00af4 Verbesserung der GUI
All checks were successful
release-tag / release-image (push) Successful in 1m47s
2025-06-16 14:37:21 +02:00
6ed9af46a3 Layout Changes
All checks were successful
release-tag / release-image (push) Successful in 1m36s
2025-06-16 11:32:31 +02:00
5128e0641f Updated Metrics
All checks were successful
release-tag / release-image (push) Successful in 1m42s
2025-06-16 11:28:34 +02:00
042bbc1c27 Added Traefik-Support
All checks were successful
release-tag / release-image (push) Successful in 1m38s
2025-06-15 22:20:29 +02:00
62 changed files with 4129 additions and 89 deletions

View File

@@ -1,3 +1,4 @@
# flod # flod
First-Line-Of-Defense First-Line-Of-Defense

6
assets/css/icon.min.css vendored Normal file

File diff suppressed because one or more lines are too long

2698
assets/css/style.css Normal file

File diff suppressed because it is too large Load Diff

BIN
assets/images/back.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
assets/images/back2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

BIN
assets/images/back3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

BIN
assets/images/back4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
assets/images/back5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

BIN
assets/images/back6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
assets/images/back7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
assets/images/back8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
assets/images/back9.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

BIN
assets/images/dummy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 B

BIN
assets/images/fav.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 B

BIN
assets/images/kiwi.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

BIN
assets/images/our2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
assets/images/our3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/images/ourprice.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
assets/images/paypal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
assets/images/price.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
assets/images/price2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
assets/images/price250.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
assets/images/price3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
assets/images/price350.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/images/price4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
assets/images/price450.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
assets/images/price50.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 B

BIN
assets/images/service.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
assets/images/service2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
assets/images/service3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
assets/images/visa.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

8
assets/js/alpine.min.js vendored Normal file

File diff suppressed because one or more lines are too long

20
assets/js/chart.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
assets/js/collapse.js Normal file
View File

@@ -0,0 +1 @@
(function(){var triggers=document.querySelectorAll("[data-collapse-target]");var collapses=document.querySelectorAll("[data-collapse]");if(triggers&&collapses){Array.from(triggers).forEach(function(trigger){return Array.from(collapses).forEach(function(collapse){if(trigger.dataset.collapseTarget===collapse.dataset.collapse){trigger.addEventListener("click",function(){if(collapse.style.height&&collapse.style.height!=="0px"){collapse.style.height=0;collapse.style.overflow="hidden";trigger.removeAttribute("open")}else{collapse.style.height="".concat(collapse.children[0].clientHeight,"px");collapse.style.overflow="visible";trigger.setAttribute("open","")}})}})})}})();

50
assets/js/counter.js Normal file
View File

@@ -0,0 +1,50 @@
document.addEventListener("DOMContentLoaded", function () {
var elements = document.querySelectorAll(".scroll-counter")
elements.forEach(function (item) {
item.counterAlreadyFired = false
item.counterSpeed = item.getAttribute("data-counter-time") / 45
item.counterTarget = +item.innerText
item.counterCount = 0
item.counterStep = item.counterTarget / item.counterSpeed
item.updateCounter = function () {
console.log(45);
item.counterCount = item.counterCount + item.counterStep
item.innerText = Math.ceil(item.counterCount)
if (item.counterCount < item.counterTarget) {
setTimeout(item.updateCounter, item.counterSpeed)
} else {
item.innerText = item.counterTarget
}
}
})
var isElementVisible = function isElementVisible(el) {
var scroll = window.scrollY || window.pageYOffset
var boundsTop = el.getBoundingClientRect().top + scroll
var viewport = {
top: scroll,
bottom: scroll + window.innerHeight,
}
var bounds = {
top: boundsTop,
bottom: boundsTop + el.clientHeight,
}
return (
(bounds.bottom >= viewport.top && bounds.bottom <= viewport.bottom) ||
(bounds.top <= viewport.bottom && bounds.top >= viewport.top)
)
}
var handleScroll = function handleScroll() {
elements.forEach(function (item, id) {
if (true === item.counterAlreadyFired) return
if (!isElementVisible(item)) return
item.updateCounter()
item.counterAlreadyFired = true
})
}
window.addEventListener("scroll", handleScroll)
})

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,60 +1,110 @@
services: services:
flodpodmaster: flodpodmaster:
image: git.send.nrw/sendnrw/flod-pod:latest image: git.send.nrw/sendnrw/flod-pod:latest
container_name: ipblock-master container_name: flodpodmaster
labels:
- traefik.enable=true
- traefik.http.routers.flodpodmaster.rule=Host(`flod-proxy.send.nrw`)
- traefik.http.services.flodpodmaster.loadbalancer.server.port=8080
- traefik.http.routers.flodpodmaster.entrypoints=websecure
- traefik.http.routers.flodpodmaster.tls=true
- traefik.http.routers.flodpodmaster.tls.certresolver=letsencrypt
- traefik.http.middlewares.flodpodmaster0-redirect.redirectscheme.scheme=https
- traefik.http.middlewares.flodpodmaster0-redirect.redirectscheme.permanent=true
- traefik.http.routers.flodpodmaster0.rule=Host(`flod-proxy.send.nrw`)
- traefik.http.routers.flodpodmaster0.entrypoints=web
- traefik.http.routers.flodpodmaster0.middlewares=flodpodmaster0-redirect
- traefik.protocol=http
depends_on:
- flodredis
networks: networks:
- flod_nw - traefik-net
environment: environment:
# Beispiel mehrere Listen in einer Kategorie „spam“ # Beispiel mehrere Listen in einer Kategorie „spam“
BLOCKLIST_MODE: master BLOCKLIST_MODE: master
REDIS_ADDR: redis:6379 REDIS_ADDR: flodredis:6379
HASH_NAME: bl:flodpod HASH_NAME: bl:flodpod
MASTER_URL: https://flod-proxy.send.nrw MASTER_URL: https://flod-proxy.send.nrw
#ports: #ports:
#- "8080:8080" # <host>:<container> #- "8080:8080" # <host>:<container>
restart: unless-stopped restart: unless-stopped
api: flodmaster:
image: git.send.nrw/sendnrw/flod:latest image: git.send.nrw/sendnrw/flod:latest
container_name: ipblock-api container_name: flodmaster
labels:
- traefik.enable=true
- traefik.http.routers.flodmaster.rule=Host(`flod.send.nrw`)
- traefik.http.services.flodmaster.loadbalancer.server.port=8080
- traefik.http.routers.flodmaster.entrypoints=websecure
- traefik.http.routers.flodmaster.tls=true
- traefik.http.routers.flodmaster.tls.certresolver=letsencrypt
- traefik.http.middlewares.flodmaster0-redirect.redirectscheme.scheme=https
- traefik.http.middlewares.flodmaster0-redirect.redirectscheme.permanent=true
- traefik.http.routers.flodmaster0.rule=Host(`flod.send.nrw`)
- traefik.http.routers.flodmaster0.entrypoints=web
- traefik.http.routers.flodmaster0.middlewares=flodmaster0-redirect
- traefik.protocol=http
networks: networks:
- flod_nw - traefik-net
depends_on: depends_on:
- redis - flodredis
- flodimporter
environment: environment:
# Redis-Adresse schon per Docker-Netzwerk korrekt: # Redis-Adresse schon per Docker-Netzwerk korrekt:
REDIS_ADDR: redis:6379 REDIS_ADDR: flodredis:6379
ROLE: worker ROLE: worker
TTL_HOURS: "720" TTL_HOURS: "720"
#ports: FLOD_IMPORT_URL: http://flodimporter:8080
#ports:
#- "8080:8080" # <host>:<container> #- "8080:8080" # <host>:<container>
restart: unless-stopped restart: unless-stopped
flodimporter:
redis: image: git.send.nrw/sendnrw/flod-ipv64-parser:latest
image: redis:7-alpine container_name: flodimporter
container_name: ipblock-redis depends_on:
ports: - flodredis
- "6379:6379" labels:
networks: - traefik.enable=true
- flod_nw - traefik.http.routers.flodimporter.rule=Host(`flod-import.send.nrw`)
command: ["redis-server", "/usr/local/etc/redis/redis.conf"] - traefik.http.services.flodimporter.loadbalancer.server.port=8080
volumes: - traefik.http.routers.flodimporter.entrypoints=websecure
- redis-data:/data # falls du doch Persistence willst - traefik.http.routers.flodimporter.tls=true
- /home/groot/flod/redis.conf:/usr/local/etc/redis/redis.conf:ro - traefik.http.routers.flodimporter.tls.certresolver=letsencrypt
restart: unless-stopped - traefik.http.middlewares.flodimporter0-redirect.redirectscheme.scheme=https
- traefik.http.middlewares.flodimporter0-redirect.redirectscheme.permanent=true
newt: - traefik.http.routers.flodimporter0.rule=Host(`flod-import.send.nrw`)
image: fosrl/newt - traefik.http.routers.flodimporter0.entrypoints=web
container_name: newt - traefik.http.routers.flodimporter0.middlewares=flodimporter0-redirect
networks: - traefik.protocol=http
- flod_nw
restart: unless-stopped
environment: environment:
- PANGOLIN_ENDPOINT= SERVEONLY: "1"
- NEWT_ID= DELAY: ""
- NEWT_SECRET= OUTDIR: ""
LIST: ""
LISTEN: :8080
PREFIX: http://flodimporter:8080
networks:
- traefik-net
restart: unless-stopped
volumes:
- importer-lists:/lists
flodredis:
image: redis:7-alpine
container_name: flodredis
ports:
- 6379:6379
networks:
- traefik-net
command:
- redis-server
- /usr/local/etc/redis/redis.conf
volumes:
- redis-data:/data # falls du doch Persistence willst
- /docker/flod_redis/redis.conf:/usr/local/etc/redis/redis.conf:ro
restart: unless-stopped
networks: networks:
flod_nw: traefik-net:
external: true
volumes: volumes:
redis-data: redis-data: null
importer-lists: null

6
go.mod
View File

@@ -1,10 +1,10 @@
module git.send.nrw/sendnrw/flod module git.send.nrw/sendnrw/flod
go 1.24.3 go 1.24.4
require ( require (
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/prometheus/client_golang v1.22.0 github.com/prometheus/client_golang v1.22.0
github.com/redis/go-redis/v9 v9.10.0
) )
require ( require (
@@ -15,7 +15,7 @@ require (
github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect
github.com/redis/go-redis/v9 v9.10.0 // indirect github.com/yl2chen/cidranger v1.0.2 // indirect
golang.org/x/sys v0.30.0 // indirect golang.org/x/sys v0.30.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect google.golang.org/protobuf v1.36.5 // indirect
) )

27
go.sum
View File

@@ -1,13 +1,26 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
@@ -18,7 +31,17 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs= github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs=
github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU=
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

1
lists/lists.json Normal file
View File

@@ -0,0 +1 @@
{}

834
main.go
View File

@@ -5,9 +5,13 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"log"
"net" "net"
"net/http" "net/http"
"net/netip" "net/netip"
"os"
"path/filepath"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -17,39 +21,188 @@ import (
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
) )
var (
// Requests & Responses & Inflight & Duration
reqTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "ipcheck_requests_total",
Help: "Total HTTP requests by handler",
},
[]string{"handler"},
)
respTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "ipcheck_http_responses_total",
Help: "HTTP responses by handler and code",
},
[]string{"handler", "code"},
)
inflight = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "ipcheck_requests_inflight",
Help: "Inflight HTTP requests",
},
)
reqDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "ipcheck_request_duration_seconds",
Help: "Request duration seconds",
// Wähle Buckets ähnlich deinem manuellen Histogramm
Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10},
},
[]string{"handler"},
)
// Importer
importCycles = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "ipcheck_import_cycles_total",
Help: "Completed import cycles",
},
)
importLastSuccess = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "ipcheck_import_last_success_timestamp_seconds",
Help: "Last successful import Unix time",
},
)
importErrors = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "ipcheck_import_errors_total",
Help: "Import errors by category",
},
[]string{"category"},
)
importDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "ipcheck_import_duration_seconds",
Help: "Import duration by category",
Buckets: []float64{0.5, 1, 2, 5, 10, 30, 60, 120, 300},
},
[]string{"category"},
)
// Bereits vorhanden: blocklistHashSizes (GaugeVec)
catalogCategories = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "ipcheck_catalog_categories_total",
Help: "Number of categories in catalog",
},
)
// Honeypot-Teile hast du im zweiten Projekt nicht → weglassen oder später ergänzen
whitelistTotal = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "ipcheck_whitelist_total",
Help: "Whitelisted IPs",
},
)
traefikBlocks = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "ipcheck_traefik_blocks_total",
Help: "Traefik blocks due to matches",
},
)
downloads = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "ipcheck_downloads_total",
Help: "Downloads served by category",
},
[]string{"category"},
)
manualBlacklistSize = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "ipcheck_manual_blacklist_size",
Help: "Manual blacklist size",
},
)
)
func init() {
prometheus.MustRegister(
reqTotal, respTotal, inflight, reqDuration,
importCycles, importLastSuccess, importErrors, importDuration,
blocklistHashSizes, catalogCategories, whitelistTotal,
traefikBlocks, downloads, manualBlacklistSize, checkBlocked, checkWhitelist,
)
// Deine existierenden Counter:
// checkRequests, checkBlocked, checkWhitelist sind okay können bleiben.
}
type statusRecorder struct {
http.ResponseWriter
code int
}
func (w *statusRecorder) WriteHeader(code int) {
w.code = code
w.ResponseWriter.WriteHeader(code)
}
func instrumentHandler(name string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
inflight.Inc()
start := time.Now()
rec := &statusRecorder{ResponseWriter: w, code: 200}
reqTotal.WithLabelValues(name).Inc()
next.ServeHTTP(rec, r)
inflight.Dec()
reqDuration.WithLabelValues(name).Observe(time.Since(start).Seconds())
respTotal.WithLabelValues(name, fmt.Sprintf("%d", rec.code)).Inc()
})
}
func instrumentFunc(name string, fn http.HandlerFunc) http.Handler {
return instrumentHandler(name, http.HandlerFunc(fn))
}
// --------------------------------------------------
//
// --------------------------------------------------
// Redis + Context // Redis + Context
var ctx = context.Background() var ctx = context.Background()
var rdb = redis.NewClient(&redis.Options{
Addr: "redis:6379", // ──────────────────────────────────────────────────────────────────────────────
}) // Helpers
// ──────────────────────────────────────────────────────────────────────────────
// ExportListJSON schreibt die Map als prettified JSONDatei.
func ExportListJSON(path string, m map[string]string) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
return enc.Encode(m)
}
// ImportListJSON liest eine JSONDatei und gibt map[string]string zurück.
func ImportListJSON(path string) (map[string]string, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var m map[string]string
if err := json.NewDecoder(f).Decode(&m); err != nil {
return nil, err
}
return m, nil
}
// URLs der Blocklisten // URLs der Blocklisten
var blocklistURLs = map[string]string{ var blocklistURLs = map[string]string{
"firehol": "https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level1.netset", "bitwire": "https://raw.githubusercontent.com/bitwire-it/ipblocklist/refs/heads/main/ip-list.txt",
"bitwire": "https://raw.githubusercontent.com/bitwire-it/ipblocklist/refs/heads/main/ip-list.txt",
"RU": "https://ipv64.net/blocklists/countries/ipv64_blocklist_RU.txt",
"CN": "https://ipv64.net/blocklists/countries/ipv64_blocklist_CN.txt",
"blocklist_de_ssh": "https://lists.blocklist.de/lists/ssh.txt",
"blocklist_de_mail": "https://lists.blocklist.de/lists/mail.txt",
"blocklist_de_apache": "https://lists.blocklist.de/lists/apache.txt",
"blocklist_de_imap": "https://lists.blocklist.de/lists/imap.txt",
"blocklist_de_ftp": "https://lists.blocklist.de/lists/ftp.txt",
"blocklist_de_sip": "https://lists.blocklist.de/lists/sip.txt",
"blocklist_de_bots": "https://lists.blocklist.de/lists/bots.txt",
"blocklist_de_strongips": "https://lists.blocklist.de/lists/strongips.txt",
"blocklist_de_bruteforcelogin": "https://lists.blocklist.de/lists/bruteforcelogin.txt",
"firehol_org_botscout_30d": "https://iplists.firehol.org/files/botscout_30d.ipset",
"firehol_org_cleantalk_30d": "https://iplists.firehol.org/files/cleantalk_30d.ipset",
"firehol_org_cleantalk_new_30d": "https://iplists.firehol.org/files/cleantalk_new_30d.ipset",
"firehol_org_abuse_30d": "https://iplists.firehol.org/files/firehol_abusers_30d.netset",
"firehol_org_gpf_comics": "https://iplists.firehol.org/files/gpf_comics.ipset",
"firehol_org_stopforumspam_365d": "https://iplists.firehol.org/files/stopforumspam_365d.ipset",
"firehol_org_tor_exit_30d": "https://iplists.firehol.org/files/tor_exits_30d.ipset",
"firehol_org_shield_30d": "https://iplists.firehol.org/files/dshield_30d.netset",
"firehol_org_firehol_webserver": "https://iplists.firehol.org/files/firehol_webserver.netset",
"firehol_org_php_dictionary_30d": "https://iplists.firehol.org/files/php_dictionary_30d.ipset",
"firehol_org_php_harvesters_30d": "https://iplists.firehol.org/files/php_harvesters_30d.ipset",
"firehol_org_php_spammers_30d": "https://iplists.firehol.org/files/php_spammers_30d.ipset",
} }
// Präfix-Cache // Präfix-Cache
@@ -60,15 +213,11 @@ type prefixCacheEntry struct {
var ( var (
prefixCache = map[string]prefixCacheEntry{} prefixCache = map[string]prefixCacheEntry{}
prefixCacheMu sync.Mutex prefixCacheMu sync.RWMutex
) )
// Prometheus Metriken // Prometheus Metriken
var ( var (
checkRequests = prometheus.NewCounter(prometheus.CounterOpts{
Name: "ipcheck_requests_total",
Help: "Total IP check requests",
})
checkBlocked = prometheus.NewCounter(prometheus.CounterOpts{ checkBlocked = prometheus.NewCounter(prometheus.CounterOpts{
Name: "ipcheck_blocked_total", Name: "ipcheck_blocked_total",
Help: "Total blocked IPs", Help: "Total blocked IPs",
@@ -77,11 +226,18 @@ var (
Name: "ipcheck_whitelisted_total", Name: "ipcheck_whitelisted_total",
Help: "Total whitelisted IPs", Help: "Total whitelisted IPs",
}) })
blocklistHashSizes = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "ipcheck_blocklist_hash_size",
Help: "Number of entries in each category",
},
[]string{"category"},
)
) )
func init() { /*func init() {
prometheus.MustRegister(checkRequests, checkBlocked, checkWhitelist) prometheus.MustRegister(checkBlocked, checkWhitelist)
} }*/
// Main // Main
func main() { func main() {
@@ -92,17 +248,137 @@ func main() {
} }
// Server // Server
http.HandleFunc("/", handleGUI) http.Handle("/", instrumentFunc("gui", checkhtml))
http.HandleFunc("/whitelist", handleWhitelist) http.Handle("/admin", instrumentFunc("admin", handleGUI))
http.HandleFunc("/check/", handleCheck) http.Handle("/download/", instrumentFunc("download", handleDownload))
http.Handle("/whitelist", instrumentFunc("whitelist", handleWhitelist))
http.Handle("/check/", instrumentFunc("check", handleCheck))
http.Handle("/traefik", instrumentFunc("traefik", handleTraefik))
http.Handle("/metrics", promhttp.Handler()) http.Handle("/metrics", promhttp.Handler())
go func() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
updateBlocklistMetrics()
<-ticker.C
}
}()
fmt.Println("Server läuft auf :8080") fmt.Println("Server läuft auf :8080")
http.ListenAndServe(":8080", nil) http.ListenAndServe(":8080", nil)
} }
func clientIPFromHeaders(r *http.Request) (netip.Addr, error) {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
parts := strings.Split(xff, ",")
s := strings.TrimSpace(parts[0])
if a, err := netip.ParseAddr(s); err == nil {
return a.Unmap(), nil
}
}
if xr := r.Header.Get("X-Real-Ip"); xr != "" {
if a, err := netip.ParseAddr(strings.TrimSpace(xr)); err == nil {
return a.Unmap(), nil
}
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err == nil {
if a, err := netip.ParseAddr(host); err == nil {
return a.Unmap(), nil
}
}
return netip.Addr{}, fmt.Errorf("cannot determine client ip")
}
func updateBlocklistMetrics() {
rdb := redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_ADDR"),
DB: 0,
Username: os.Getenv("REDIS_USER"),
Password: os.Getenv("REDIS_PASS"),
})
// Blocklist-Hash-Größen pro Kategorie
for cat := range blocklistURLs {
key := "bl:" + cat
count, err := rdb.HLen(ctx, key).Result()
if err != nil {
fmt.Printf("❌ Redis HLen Error for %s: %v\n", key, err)
continue
}
blocklistHashSizes.WithLabelValues(cat).Set(float64(count))
}
// Whitelist gesamt (wenn als Keys "wl:<ip>" gespeichert)
if n, err := rdb.Keys(ctx, "wl:*").Result(); err == nil {
whitelistTotal.Set(float64(len(n)))
}
// Manuelle Blacklist, falls vorhanden
if n, err := rdb.HLen(ctx, "bl:manual").Result(); err == nil {
manualBlacklistSize.Set(float64(n))
}
}
type target struct {
Name, URL string
}
func fetchAndSave(client *http.Client, t target, outDir string) error {
fileName := filepath.Base(t.URL)
if fileName == "" {
fileName = strings.ReplaceAll(strings.ToLower(strings.ReplaceAll(t.Name, " ", "_")), "..", "")
}
dst := filepath.Join(outDir, fileName)
log.Printf("Downloading %-40s → %s", t.Name, dst)
resp, err := client.Get(t.URL)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bad HTTP status: %s", resp.Status)
}
tmp := dst + ".part"
f, err := os.Create(tmp)
if err != nil {
return err
}
if _, err := io.Copy(f, resp.Body); err != nil {
f.Close()
os.Remove(tmp)
return err
}
f.Close()
return os.Rename(tmp, dst)
}
// Import-Logik // Import-Logik
func importBlocklists() error { func importBlocklists() error {
startAll := time.Now()
importCycles.Inc()
client := &http.Client{Timeout: 60 * time.Second}
t := target{Name: "Catalog", URL: os.Getenv("FLOD_IMPORT_URL")}
if err := os.MkdirAll("/lists/", 0o755); err != nil {
fmt.Println("creating output dir", err)
}
if err := fetchAndSave(client, t, "/lists/"); err != nil {
log.Printf("ERROR %s → %v", t.URL, err)
}
fileName := filepath.Base(t.URL)
if fileName == "" {
fileName = strings.ReplaceAll(strings.ToLower(strings.ReplaceAll(t.Name, " ", "_")), "..", "")
}
blocklistURLs, _ = ImportListJSON("/lists/" + fileName)
catalogCategories.Set(float64(len(blocklistURLs)))
var wg sync.WaitGroup var wg sync.WaitGroup
errCh := make(chan error, len(blocklistURLs)) errCh := make(chan error, len(blocklistURLs))
@@ -110,15 +386,25 @@ func importBlocklists() error {
wg.Add(1) wg.Add(1)
go func(c, u string) { go func(c, u string) {
defer wg.Done() defer wg.Done()
start := time.Now()
if err := importCategory(c, u); err != nil { if err := importCategory(c, u); err != nil {
importErrors.WithLabelValues(c).Inc()
errCh <- fmt.Errorf("%s: %v", c, err) errCh <- fmt.Errorf("%s: %v", c, err)
} }
importDuration.WithLabelValues(c).Observe(time.Since(start).Seconds())
}(cat, url) }(cat, url)
} }
wg.Wait() wg.Wait()
close(errCh) close(errCh)
// Erfolgstimestamp nur setzen, wenn keine Fehler:
if len(errCh) == 0 {
importLastSuccess.Set(float64(time.Now().Unix()))
}
_ = startAll // (falls du Gesamtzeit noch extra messen willst)
for err := range errCh { for err := range errCh {
fmt.Println("❌", err) fmt.Println("❌", err)
} }
@@ -127,11 +413,18 @@ func importBlocklists() error {
return fmt.Errorf("Blocklisten-Import teilweise fehlgeschlagen") return fmt.Errorf("Blocklisten-Import teilweise fehlgeschlagen")
} }
fmt.Println("✅ Blocklisten-Import abgeschlossen") fmt.Println("✅ Blocklisten-Import abgeschlossen")
fmt.Println(blocklistURLs)
blocklistURLs["flodpod"] = "null" blocklistURLs["flodpod"] = "null"
return nil return nil
} }
func importCategory(cat, url string) error { func importCategory(cat, url string) error {
var rdb = redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_ADDR"),
DB: 0,
Username: os.Getenv("REDIS_USER"),
Password: os.Getenv("REDIS_PASS"),
})
fmt.Printf("⬇️ Lade %s (%s)\n", cat, url) fmt.Printf("⬇️ Lade %s (%s)\n", cat, url)
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
@@ -201,6 +494,12 @@ func normalizePrefix(s string) (string, bool) {
} }
func handleWhitelist(w http.ResponseWriter, r *http.Request) { func handleWhitelist(w http.ResponseWriter, r *http.Request) {
var rdb = redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_ADDR"),
DB: 0,
Username: os.Getenv("REDIS_USER"),
Password: os.Getenv("REDIS_PASS"),
})
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed) http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return return
@@ -239,7 +538,12 @@ func handleWhitelist(w http.ResponseWriter, r *http.Request) {
// Check-Handler // Check-Handler
func handleCheck(w http.ResponseWriter, r *http.Request) { func handleCheck(w http.ResponseWriter, r *http.Request) {
checkRequests.Inc() var rdb = redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_ADDR"),
DB: 0,
Username: os.Getenv("REDIS_USER"),
Password: os.Getenv("REDIS_PASS"),
})
ipStr := strings.TrimPrefix(r.URL.Path, "/check/") ipStr := strings.TrimPrefix(r.URL.Path, "/check/")
ip, err := netip.ParseAddr(ipStr) ip, err := netip.ParseAddr(ipStr)
if err != nil { if err != nil {
@@ -248,7 +552,7 @@ func handleCheck(w http.ResponseWriter, r *http.Request) {
} }
var cats []string var cats []string
for a, _ := range blocklistURLs { for a := range blocklistURLs {
cats = append(cats, a) cats = append(cats, a)
} }
@@ -273,8 +577,60 @@ func handleCheck(w http.ResponseWriter, r *http.Request) {
}) })
} }
// Check-Handler
func handleTraefik(w http.ResponseWriter, r *http.Request) {
rdb := redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_ADDR"),
DB: 0,
Username: os.Getenv("REDIS_USER"),
Password: os.Getenv("REDIS_PASS"),
})
ip, err := clientIPFromHeaders(r)
if err != nil {
http.Error(w, "invalid IP", http.StatusBadRequest)
return
}
// Kategorien dynamisch aus blocklistURLs
cats := make([]string, 0, len(blocklistURLs))
for c := range blocklistURLs {
cats = append(cats, c)
}
matches, err := checkIP(ip, cats)
if err != nil {
http.Error(w, "server error", http.StatusInternalServerError)
return
}
// Whitelist check (wie gehabt)
if len(matches) == 0 {
wl, _ := rdb.Exists(ctx, "wl:"+ip.String()).Result()
if wl > 0 {
checkWhitelist.Inc()
}
}
if len(matches) > 0 {
checkBlocked.Inc()
traefikBlocks.Inc()
errorhtml(w, r)
//http.Error(w, "blocked", http.StatusTooManyRequests)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK"))
}
// Check-Logik // Check-Logik
func checkIP(ip netip.Addr, cats []string) ([]string, error) { func checkIP(ip netip.Addr, cats []string) ([]string, error) {
var rdb = redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_ADDR"),
DB: 0,
Username: os.Getenv("REDIS_USER"),
Password: os.Getenv("REDIS_PASS"),
})
wl, err := rdb.Exists(ctx, "wl:"+ip.String()).Result() wl, err := rdb.Exists(ctx, "wl:"+ip.String()).Result()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -300,6 +656,12 @@ func checkIP(ip netip.Addr, cats []string) ([]string, error) {
} }
func loadCategoryPrefixes(cat string) ([]netip.Prefix, error) { func loadCategoryPrefixes(cat string) ([]netip.Prefix, error) {
var rdb = redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_ADDR"),
DB: 0,
Username: os.Getenv("REDIS_USER"),
Password: os.Getenv("REDIS_PASS"),
})
prefixCacheMu.Lock() prefixCacheMu.Lock()
defer prefixCacheMu.Unlock() defer prefixCacheMu.Unlock()
entry, ok := prefixCache[cat] entry, ok := prefixCache[cat]
@@ -322,7 +684,8 @@ func loadCategoryPrefixes(cat string) ([]netip.Prefix, error) {
} }
prefixCache[cat] = prefixCacheEntry{ prefixCache[cat] = prefixCacheEntry{
prefixes: prefixes, prefixes: prefixes,
expireAt: time.Now().Add(1 * time.Second), expireAt: time.Now().Add(10 * time.Minute),
//Hier geändert von 1 * time.Second
} }
return prefixes, nil return prefixes, nil
} }
@@ -333,6 +696,43 @@ func writeJSON(w http.ResponseWriter, v any) {
_ = json.NewEncoder(w).Encode(v) _ = json.NewEncoder(w).Encode(v)
} }
func handleDownload(w http.ResponseWriter, r *http.Request) {
var rdb = redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_ADDR"),
DB: 0,
Username: os.Getenv("REDIS_USER"),
Password: os.Getenv("REDIS_PASS"),
})
cat := strings.TrimPrefix(r.URL.Path, "/download/")
if cat == "" {
http.Error(w, "category missing", http.StatusBadRequest)
return
}
// Prüfen, ob Kategorie existiert
if _, ok := blocklistURLs[cat]; !ok {
http.Error(w, "unknown category", http.StatusNotFound)
return
}
// Alle Einträge holen
keys, err := rdb.HKeys(ctx, "bl:"+cat).Result()
if err != nil {
http.Error(w, "redis error", http.StatusInternalServerError)
return
}
// Header für Download setzen
downloads.WithLabelValues(cat).Inc()
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.txt\"", cat))
// Zeilenweise schreiben
for _, k := range keys {
_, _ = fmt.Fprintln(w, k)
}
}
func handleGUI(w http.ResponseWriter, r *http.Request) { func handleGUI(w http.ResponseWriter, r *http.Request) {
html := ` html := `
<!DOCTYPE html> <!DOCTYPE html>
@@ -341,9 +741,9 @@ func handleGUI(w http.ResponseWriter, r *http.Request) {
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>IP Checker GUI</title> <title>IP Checker GUI</title>
<style> <style>
body { font-family: sans-serif; max-width: 600px; margin: auto; padding: 2em; background: #f9fafb; } body { font-family: sans-serif; max-width: 1000px; margin: auto; padding: 2em; background: #f9fafb; }
h1 { font-size: 1.5em; margin-bottom: 1em; } h1 { font-size: 1.5em; margin-bottom: 1em; }
input, button { padding: 0.7em; margin: 0.3em 0; width: 100%; border-radius: 0.4em; border: 1px solid #ccc; } input, button { padding: 0.7em; margin: 0.3em 0; width: 100%; border-radius: 0.4em; border: 1px solid #ccc; box-sizing: border-box; }
button { background: #2563eb; color: white; border: none; cursor: pointer; } button { background: #2563eb; color: white; border: none; cursor: pointer; }
button:hover { background: #1d4ed8; } button:hover { background: #1d4ed8; }
#result, #metrics, #history { background: white; border: 1px solid #ddd; padding: 1em; border-radius: 0.4em; margin-top: 1em; white-space: pre-wrap; } #result, #metrics, #history { background: white; border: 1px solid #ddd; padding: 1em; border-radius: 0.4em; margin-top: 1em; white-space: pre-wrap; }
@@ -358,11 +758,13 @@ func handleGUI(w http.ResponseWriter, r *http.Request) {
<h2>Ergebnis</h2> <h2>Ergebnis</h2>
<div id="result">No Request</div> <div id="result">No Request</div>
<h2>Check History</h2>
<div id="history">No history</div>
<h2>Prometheus Metrics</h2> <h2>Prometheus Metrics</h2>
<div id="metrics">Loading...</div> <div id="metrics">Loading...</div>
<h2>Check History</h2>
<div id="history">No history</div>
<script> <script>
async function checkIP() { async function checkIP() {
@@ -424,3 +826,343 @@ func handleGUI(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write([]byte(html)) _, _ = w.Write([]byte(html))
} }
func checkhtml(w http.ResponseWriter, r *http.Request) {
html := `<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FLODP IP Check</title>
<style>
:root{
--bg:#f6f7f9;--text:#1f2937;--muted:#6b7280;--card:#ffffff;
--success:#22c55e;--danger:#ef4444;--accent:#2563eb;--border:#e5e7eb;
}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;background:var(--bg);color:var(--text);font:16px/1.5 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,"Helvetica Neue",Arial}
.wrap{max-width:980px;margin:0 auto;padding:40px 16px 64px}
header{display:flex;align-items:center;gap:12px;flex-wrap:wrap;}
h1{font-size:clamp(24px,4vw,38px);font-weight:700;margin:0}
.pill{display:inline-flex;align-items:center;gap:8px;padding:6px 10px;border-radius:999px;border:1px solid var(--border);background:#fff;font-weight:600;font-size:14px;color:#111}
.pill small{font-weight:500;color:var(--muted)}
p.lead{margin:12px 0 24px;color:var(--muted)}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:18px;margin-top:16px}
.node{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px;display:flex;flex-direction:column;gap:12px;box-shadow:0 6px 18px rgba(0,0,0,.04)}
.node h3{margin:0 0 4px;font-size:16px}
.sub{margin:0;color:var(--muted);font-size:14px}
.status-badge{display:inline-flex;align-items:center;gap:8px;padding:6px 10px;border-radius:999px;border:1px solid var(--border);background:#fff;font-weight:700;width:max-content}
.status-badge.ok{color:var(--success);border-color:#bbf7d0;background:#f0fdf4}
.status-badge.err{color:var(--danger);border-color:#fecaca;background:#fef2f2}
.row{display:flex;gap:12px;flex-wrap:wrap}
.field{display:flex;flex-direction:column;gap:6px;flex:1;min-width:220px}
label{font-weight:600}
input[type="text"]{
padding:12px;border-radius:10px;border:1px solid var(--border);outline:none;background:#fff;
}
input[type="text"]:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(37,99,235,.15)}
.btn{border:1px solid var(--border);background:#fff;font-weight:600;padding:10px 14px;border-radius:10px;cursor:pointer}
.btn.primary{background:var(--accent);border-color:var(--accent);color:#fff}
.btn:disabled{opacity:.6;cursor:not-allowed}
.muted{color:var(--muted)}
.code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;white-space:pre-wrap;background:#f8fafc;border:1px solid var(--border);padding:12px;border-radius:10px}
.chip{display:inline-block;margin:4px 6px 0 0;padding:4px 8px;border-radius:999px;background:#eef2ff;border:1px solid #c7d2fe;color:#3730a3;font-weight:600;font-size:12px}
footer{margin-top:40px;color:var(--muted);font-size:13px;text-align:center}
.hint{font-size:13px;color:var(--muted)}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>IP Check</h1>
<span class="pill">FLODP <small>Security Utility</small></span>
</header>
<p class="lead">Prüfe schnell, ob eine IP in den Blocklisten gelistet ist. Die Abfrage nutzt den Endpunkt <code>/check/&lt;ip&gt;</code>.</p>
<section class="grid" aria-label="IP-Check">
<!-- Formular -->
<article class="node">
<h3>Anfrage</h3>
<p class="sub">Sende eine Abfrage an <code>/check/&lt;ip&gt;</code></p>
<form id="checkForm" class="row" novalidate>
<div class="field">
<label for="ip">IP-Adresse</label>
<input id="ip" name="ip" type="text" placeholder="z. B. 203.0.113.42 oder 2001:db8::1" autocomplete="off" required>
<small class="hint">IPv4 oder IPv6. Es erfolgt eine leichte Client-Validierung.</small>
</div>
<div class="row" style="align-items:flex-end">
<button id="btnCheck" class="btn primary" type="submit">Check ausführen</button>
<button id="btnClear" class="btn" type="button">Zurücksetzen</button>
</div>
</form>
</article>
<!-- Ergebnis -->
<article class="node" id="resultCard" aria-live="polite">
<h3>Ergebnis</h3>
<div id="statusBadge" class="status-badge" style="display:none"></div>
<div id="summary" class="muted">Noch keine Abfrage durchgeführt.</div>
<div id="catsWrap" style="display:none">
<strong>Kategorien:</strong>
<div id="cats"></div>
</div>
<details id="rawWrap" style="margin-top:8px; display:none">
<summary><strong>Rohdaten (Response JSON)</strong></summary>
<pre id="raw" class="code"></pre>
</details>
</article>
</section>
<footer>
<span>© First-Line-Of-Defense-Project</span>
</footer>
</div>
<script>
const form = document.getElementById('checkForm');
const ipInput = document.getElementById('ip');
const btnCheck = document.getElementById('btnCheck');
const btnClear = document.getElementById('btnClear');
const statusBadge = document.getElementById('statusBadge');
const summary = document.getElementById('summary');
const catsWrap = document.getElementById('catsWrap');
const cats = document.getElementById('cats');
const rawWrap = document.getElementById('rawWrap');
const raw = document.getElementById('raw');
// Simple IPv4/IPv6 Check (nicht perfekt, aber hilfreich)
function looksLikeIP(value){
const v = value.trim();
const ipv4 = /^(25[0-5]|2[0-4]\d|[01]?\d\d?)(\.(25[0-5]|2[0-4]\d|[01]?\d\d?)){3}$/;
const ipv6 = /^([0-9a-f]{0,4}:){2,7}[0-9a-f]{0,4}$/i; // sehr tolerant
return ipv4.test(v) || ipv6.test(v);
}
function setLoading(loading){
btnCheck.disabled = loading;
btnCheck.textContent = loading ? 'Wird geprüft…' : 'Check ausführen';
ipInput.disabled = loading;
}
function setStatus(ok, text){
statusBadge.style.display = 'inline-flex';
statusBadge.className = 'status-badge ' + (ok ? 'ok' : 'err');
statusBadge.textContent = ok ? 'OK • not listed' : 'BLOCKED • listed';
summary.textContent = text;
}
function resetUI(){
statusBadge.style.display = 'none';
statusBadge.className = 'status-badge';
summary.textContent = 'Noch keine Abfrage durchgeführt.';
catsWrap.style.display = 'none';
cats.innerHTML = '';
rawWrap.style.display = 'none';
raw.textContent = '';
}
btnClear.addEventListener('click', () => {
form.reset();
resetUI();
ipInput.focus();
});
form.addEventListener('submit', async (e) => {
e.preventDefault();
const ip = ipInput.value.trim();
if(!looksLikeIP(ip)){
ipInput.focus();
ipInput.select();
summary.textContent = 'Bitte eine gültige IPv4- oder IPv6-Adresse eingeben.';
statusBadge.style.display = 'inline-flex';
statusBadge.className = 'status-badge err';
statusBadge.textContent = 'Ungültige IP';
catsWrap.style.display = 'none';
rawWrap.style.display = 'none';
return;
}
setLoading(true);
try{
const res = await fetch('/check/' + encodeURIComponent(ip));
const data = await res.json();
// Erwartete Struktur: { ip: "...", blocked: bool, categories: [] }
const ok = data && data.blocked === false;
setStatus(ok, ok
? 'Die IP ' + data.ip + ' ist nicht gelistet.'
: 'Die IP ' + data.ip + ' ist gelistet.');
// Kategorien
const list = Array.isArray(data.categories) ? data.categories : [];
if(!ok && list.length > 0){
catsWrap.style.display = 'block';
cats.innerHTML = list.map(function(c){ return '<span class="chip">' + c + '</span>'; }).join('');
}else{
catsWrap.style.display = 'none';
cats.innerHTML = '';
}
// Rohdaten anzeigen
rawWrap.style.display = 'block';
raw.textContent = JSON.stringify(data, null, 2);
}catch(err){
setStatus(false, 'Fehler bei der Abfrage. Details siehe Konsole.');
console.error(err);
rawWrap.style.display = 'none';
}finally{
setLoading(false);
}
});
</script>
</body>
</html>`
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(html))
}
func errorhtml(w http.ResponseWriter, r *http.Request) {
html := `<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Blocked by the First-Line-Of-Defense-Project</title>
<style>
:root{
--bg:#f6f7f9;--text:#1f2937;--muted:#6b7280;--card:#ffffff;
--success:#22c55e;--danger:#ef4444;--accent:#2563eb;--border:#e5e7eb;
}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;background:var(--bg);color:var(--text);font:16px/1.5 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,"Helvetica Neue",Arial}
.wrap{max-width:980px;margin:0 auto;padding:40px 16px 64px}
header{display:flex;align-items:center;gap:12px;flex-wrap:wrap;}
h1{font-size:clamp(24px,4vw,38px);font-weight:700;margin:0}
.pill{display:inline-flex;align-items:center;gap:8px;padding:6px 10px;border-radius:999px;border:1px solid var(--border);background:#fff;font-weight:600;font-size:14px;color:#111}
.pill small{font-weight:500;color:var(--muted)}
p.lead{margin:12px 0 24px;color:var(--muted)}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:18px;margin-top:16px}
.node{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px;display:flex;flex-direction:column;align-items:center;gap:10px;box-shadow:0 6px 18px rgba(0,0,0,.04)}
.icon{position:relative;width:104px;height:80px;display:grid;place-items:center}
.status{position:absolute;right:-8px;bottom:-8px;width:30px;height:30px;border-radius:999px;display:grid;place-items:center;color:#fff;font-weight:800;font-size:14px}
.status.ok{background:var(--success)}
.status.err{background:var(--danger)}
.node h3{margin:6px 0 0;font-size:16px}
.node .sub{margin:0;color:var(--muted);font-size:14px}
.node .state{margin:4px 0 0;font-weight:700}
.state.ok{color:var(--success)}
.state.err{color:var(--danger)}
.actions{margin-top:28px;display:flex;gap:12px;flex-wrap:wrap}
.btn{border:1px solid var(--border);background:#fff;font-weight:600;padding:10px 14px;border-radius:10px;cursor:pointer}
.btn.primary{background:var(--accent);border-color:var(--accent);color:#fff}
.meta{margin-top:24px;color:var(--muted);font-size:13px}
footer{margin-top:40px;color:var(--muted);font-size:13px}
/* Simple, friendly SVG look */
svg{display:block}
.dim{fill:#e5e7eb}
.stroke{stroke:#9ca3af}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>You have been blocked by the First-Line-Of-Defense-Project</h1>
<span class="pill">ERROR&nbsp;403 <small>Forbidden</small></span>
</header>
<p class="lead">
Your connection attempt to the target server was blocked by the First-Line-Of-Defense-Project. Your IP address is listed on at least one blacklist.
</p>
<section class="grid" aria-label="Diagnostic chain">
<article class="node" aria-label="Browser Status">
<div class="icon" aria-hidden="true">
<svg width="88" height="62" viewBox="0 0 88 62" xmlns="http://www.w3.org/2000/svg" role="img">
<rect x="1" y="6" width="86" height="55" rx="8" fill="#fff" stroke="#d1d5db"/>
<rect x="1" y="1" width="86" height="14" rx="8" fill="#f3f4f6" stroke="#d1d5db"/>
<circle cx="10" cy="8" r="2.5" fill="#ef4444"/>
<circle cx="18" cy="8" r="2.5" fill="#f59e0b"/>
<circle cx="26" cy="8" r="2.5" fill="#22c55e"/>
</svg>
<div class="status ok" title="Functional">✓</div>
</div>
<h3>You</h3>
<p class="sub">Browser</p>
<p class="state ok">Functional</p>
</article>
<!-- Edge / Proxy -->
<article class="node" aria-label="FLODP Status">
<div class="icon" aria-hidden="true">
<svg width="96" height="64" viewBox="0 0 96 64" xmlns="http://www.w3.org/2000/svg" role="img">
<path d="M33 44h32a14 14 0 0 0 0-28 18 18 0 0 0-34-5 16 16 0 0 0-4 31z" fill="#e5e7eb" stroke="#d1d5db"/>
</svg>
<div class="status err" title="Blocked">✕</div>
</div>
<h3>FLODP-SERVICE</h3>
<p class="sub">Security-Gateway</p>
<p class="state err">Blocked your request</p>
</article>
<!-- Host / Origin -->
<article class="node" aria-label="Origin/Host Status">
<div class="icon" aria-hidden="true">
<svg width="88" height="62" viewBox="0 0 88 62" xmlns="http://www.w3.org/2000/svg" role="img">
<rect x="6" y="10" width="76" height="18" rx="4" fill="#f3f4f6" stroke="#d1d5db"/>
<circle cx="16" cy="19" r="3" fill="#9ca3af"/>
<rect x="6" y="34" width="76" height="18" rx="4" fill="#f3f4f6" stroke="#d1d5db"/>
<circle cx="16" cy="43" r="3" fill="#9ca3af"/>
</svg>
<div class="status ok" title="Functional">✓</div>
</div>
<h3>Host</h3>
<p class="sub">Origin-Server</p>
<p class="state ok">Functional</p>
</article>
</section>
<div class="actions">
<button class="btn primary" onclick="location.reload()">Try again</button>
<button class="btn" onclick="document.getElementById('details').toggleAttribute('open')">Show details</button>
</div>
<details id="details" class="meta">
<summary><strong>Technical details</strong></summary>
<ul>
<li>Error: <strong>403</strong> - Your IP address is listed on at least one blacklist. The service's security system has therefore rejected your connection.</li>
<li>Time: <span id="now">-</span></li>
</ul>
<p>Tips: Check if your system (browser, API, or similar) has a high connection frequency and has been blocked on other systems protected by FLODP.</p>
</details>
<footer>
<span>If the problem persists, contact the website operator.</span>
</footer>
</div>
<script>
(function(){
const now = new Date()
document.getElementById('now').textContent = now.toLocaleString()
})()
</script>
</body>
</html>`
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte(html))
}

433
template.html Normal file
View File

@@ -0,0 +1,433 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="assets/images/fav.png" type="image/x-icon">
<title>FLOD Project</title>
<link href="assets/css/style.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/icon.min.css" />
<style>
</style>
</head>
<body class="font-body bg-black/5">
<!-- hero section -->
<section class="py-16 bg-primary text-white">
<div class=" container mx-auto">
<div class="flex justify-center items-center">
<div class="text-center">
<h5 class="text-3xl font-bold my-5">FLOD Project
</h5>
<p>Honeypod Tracker: Identifying and logging IP addresses that interact with decoy ports.<br>Our system collects valuable data on potential attackers by monitoring unsolicited connection attempts.<br>Strengthen your network security with real-time threat intelligence.</p>
</div>
</div>
</div>
</section>
<!-- Section 2 -->
<section class="my-10">
<div class="container mx-auto">
<div class="bg-white p-8">
<nav class="bg-white p-4 rounded-md shadow-md">
<ol class="list-reset flex text-black/70">
<li>
<a href="#" class="text-primary/60 hover-underline-animation">Home</a>
<span class="mx-2">/</span>
</li>
<li>
<a href="#" class="text-primary/60 hover-underline-animation">Products</a>
<span class="mx-2">/</span>
</li>
<li>
<a href="#" class="text-primary/60 hover-underline-animation">Electronics</a>
<span class="mx-2">/</span>
</li>
<li class="text-black/50">Mobile Phones</li>
</ol>
</nav>
</div>
</div>
</section>
<!-- section 3 -->
<section class="my-10">
<div class="container mx-auto">
<div class="bg-white p-8">
<div class="text-center mb-10">
<h4 class="text-base text-primary/60 font-medium mb-1">Our lists</h4>
<h2 class="font-bold text-black/80 mb-4 text-xl lg:text-2xl">TIERS</h2>
</div>
<div class="grid gap-8 md:grid-cols-3">
<div
class="text-center bg-white border border-third rounded-xl transition-all ease-in-out duration-1000 hover:bg-third/10">
<div class="pb-8">
<h3
class="text-xl font-bold mb-8 lg:text-3xl h-32 text-white bg-third rounded-t-xl flex justify-center items-center relative after:absolute after:-bottom-2 after:left-1/2 after:-translate-x-1/2 after:w-5 after:h-5 after:rotate-45 after:bg-third">
IPv4 - FLOD</h3>
<h4 class="text-lg font-bold text-black/60 mb-6">Only IPv4 by FLOD</h4>
<h2 class="font-bold text-third mb-2 text-4xl">Free</h2>
<p class="text-base text-black/60 mb-6">Personal Use</p>
<ul class="text-black/40 font-medium mb-6 grid gap-3">
<li><a href="#">more than 4500 IPv4</a></li>
<li><a href="#">only IPv4</a></li>
<li><a href="#">No Collection-Lists</a></li>
</ul>
<button
class="text-base bg-third rounded-full text-white px-6 py-2 border border-third/80 transition ease-in-out duration-1000 hover:text-third hover:bg-white">Get
Started</button>
</div>
</div>
<div
class="text-center bg-white border border-fourth rounded-xl transition-all ease-in-out duration-1000 hover:bg-fourth/10">
<div class="pb-8">
<h3
class="text-xl font-bold mb-8 lg:text-3xl h-32 text-white bg-fourth rounded-t-xl flex justify-center items-center relative after:absolute after:-bottom-2 after:left-1/2 after:-translate-x-1/2 after:w-5 after:h-5 after:rotate-45 after:bg-fourth">
IPv4+6 - FLOD</h3>
<h4 class="text-lg font-bold text-black/60 mb-6">IPv4 + IPv6 by FLOD</h4>
<h2 class="font-bold text-fourth mb-2 text-4xl">Free</h2>
<p class="text-base text-black/60 mb-6">Personal Use</p>
<ul class="text-black/40 font-medium mb-6 grid gap-3">
<li><a href="#">more than 4500 IPv4</a></li>
<li><a href="#">more than 100 IPv6</a></li>
<li><a href="#">No Collection-Lists</a></li>
</ul>
<button
class="text-base bg-fourth rounded-full text-white px-6 py-2 border border-fourth/80 transition ease-in-out duration-1000 hover:text-fourth hover:bg-white">Get
Started</button>
</div>
</div>
<div
class="text-center bg-white border border-secondary rounded-xl transition-all ease-in-out duration-1000 hover:bg-secondary/10">
<div class="pb-8">
<h3
class="text-xl font-bold mb-8 lg:text-3xl h-32 text-white bg-secondary rounded-t-xl flex justify-center items-center relative after:absolute after:-bottom-2 after:left-1/2 after:-translate-x-1/2 after:w-5 after:h-5 after:rotate-45 after:bg-secondary">
IPv4+6 - F+E</h3>
<h4 class="text-lg font-bold text-black/60 mb-6">IPv4 + IPv6 by FLOD + External</h4>
<h2 class="font-bold text-secondary mb-2 text-4xl">Member</h2>
<p class="text-base text-black/60 mb-6">Personal / Business Use</p>
<ul class="text-black/40 font-medium mb-6 grid gap-3">
<li><a href="#">more than 4500 IPv4</a></li>
<li><a href="#">more than 100 IPv6</a></li>
<li><a href="#">more than 600.000 Subnets</a></li>
</ul>
<button
class="text-base bg-secondary rounded-full text-white px-6 py-2 border border-secondary/80 transition ease-in-out duration-1000 hover:text-secondary hover:bg-white">Get
Started</button>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- section 3 -->
<section class="my-10">
<div class="container mx-auto">
<div class="grid gap-8 items-center bg-white p-8 md:grid-cols-3">
<div class="grid grid-cols-2 gap-2">
<img src="assets/images/back.jpg" alt="dummy" class="rounded-xl w-full">
<img src="assets/images/back2.jpg" alt="dummy" class="rounded-xl w-full">
<img src="assets/images/back3.jpg" alt="dummy" class="rounded-xl w-full">
<img src="assets/images/back5.jpg" alt="dummy" class="rounded-xl w-full">
</div>
<div class="md:col-span-2">
<h4 class="text-base text-primary/60 font-medium mb-1">Description</h4>
<h2 class="font-bold text-black/80 mb-4 text-xl lg:text-2xl">What ist the FLOD-Project?</h2>
<p class="text-black/40 text-base mb-4">A Honeypod is a decoy system designed to attract and detect unauthorized or malicious connection attempts.
In the FLOD-Project, multiple distributed systems monitor connection attempts on various ports.
These systems are not part of any productive environment and have no legitimate services running.
Any connection to these Honeypods reliably indicates port scans, probing, or other potentially harmful activities.</p>
<button
class="text-base bg-primary mt-4 rounded-full text-white px-6 py-2 border border-primary/80 transition ease-in-out duration-1000 hover:text-primary hover:bg-white">Read
More</button>
</div>
</div>
</div>
</section>
<!-- section 4 -->
<section class="my-10">
<div class="container mx-auto">
<div class="bg-white p-8">
<div class="text-center mb-16">
<div class="md:col-span-2">
<h4 class="text-base text-primary/60 font-medium mb-1">FLOD - SYSTEM</h4>
<h2 class="font-bold text-black/80 mb-4 text-xl lg:text-2xl">Access the System!</h2>
</div>
</div>
<div class="mx-auto grid gap-8 md:w-4/6">
<div class="md:flex gap-8">
<div class="text-center mb-4 md:mb-0">
<span
class="text-2xl font-bold text-fourth/80 w-16 h-16 border-2 border-fourth !flex items-center justify-center rounded-full mx-auto md:mb-2">R</span>
<h4 class="text-base text-black/60 font-medium">Requests</h4>
</div>
<div
class="bg-fourth/10 rounded-xl relative p-6 transition-all ease-in-out duration-1000 hover:bg-fourth/20 hover:shadow-md hover:shadow-fourth/40">
<h2 class="text-lg font-bold text-black/80 mb-2 lg:text-xl">Requests</h2>
<div id="result">No Request</div>
<p class="text-black/40 text-base">Send your request and view result.</p>
</div>
</div>
<div class="md:flex gap-8">
<div class="text-center mb-4 md:mb-0">
<span
class="text-2xl font-bold text-fourth/80 w-16 h-16 border-2 border-fourth !flex items-center justify-center rounded-full mx-auto md:mb-2">M</span>
<h4 class="text-base text-black/60 font-medium">Metrics</h4>
</div>
<div
class="bg-fourth/10 rounded-xl relative p-6 transition-all ease-in-out duration-1000 hover:bg-fourth/20 hover:shadow-md hover:shadow-fourth/40">
<h2 class="text-lg font-bold text-black/80 mb-2 lg:text-xl">Metrics</h2>
<div id="metrics">Loading...</div>
<p class="text-black/40 text-base">Live metrics calculated by the system.</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- section 4 -->
<section class="my-10">
<div class="container mx-auto">
<div class="bg-white p-8">
<div class="text-center mb-10">
<div class="md:col-span-2">
<h4 class="text-base text-primary/60 font-medium mb-1 tracking-[8px]">FAQ</h4>
<h2 class="font-bold text-black/80 mb-4 text-xl lg:text-2xl">Frequently Asked Questions</h2>
<p class="text-black/40 text-base">Lorem Ipsum is simply dummy text of the printing and
typesetting industry.</p>
</div>
</div>
<div class="grid gap-8 mx-auto md:w-4/5">
<div class="grid gap-4">
<h4 class="text-lg font-medium text-third/80">Techanical</h4>
<div class="relative mb-3">
<h6 class="mb-0">
<button
class="relative border border-third bg-third/10 shadow-md shadow-third/20 rounded-lg flex items-center w-full font-semibold text-left transition-all ease-in cursor-pointer text-black/60 group"
data-collapse-target="animated-collapse-23">
<i
class="fa-solid fa-question text-xl w-14 h-14 me-2 rounded-s-lg !flex justify-center items-center bg-third/80 text-white/80"></i>
<span class="me-4">Lorem Ipsum is simply dummy text</span>
<i
class="absolute right-3 text-base text-third transition-transform fa fa-chevron-down group-open:rotate-180"></i>
</button>
</h6>
<div data-collapse="animated-collapse-23"
class="h-0 overflow-hidden transition-all duration-300 ease-in-out">
<div class="pt-4 text-sm leading-normal text-black/60">
We're not always in the position that we want to be at. We're constantly
growing. We're constantly making mistakes. We're constantly trying to
express ourselves and actualize our dreams.
</div>
</div>
</div>
<div class="relative mb-3">
<h6 class="mb-0">
<button
class="relative border border-third bg-third/10 shadow-md shadow-third/20 rounded-lg flex items-center w-full font-semibold text-left transition-all ease-in cursor-pointer text-black/60 group"
data-collapse-target="animated-collapse-24">
<i
class="fa-solid fa-question text-xl w-14 h-14 me-2 rounded-s-lg !flex justify-center items-center bg-third/80 text-white/80"></i>
<span class="me-4">Lorem Ipsum is simply dummy text</span>
<i
class="absolute right-3 text-base text-third transition-transform fa fa-chevron-down group-open:rotate-180"></i>
</button>
</h6>
<div data-collapse="animated-collapse-24"
class="h-0 overflow-hidden transition-all duration-300 ease-in-out">
<div class="pt-4 text-sm leading-normal text-black/60">
We're not always in the position that we want to be at. We're constantly
growing. We're constantly making mistakes. We're constantly trying to
express ourselves and actualize our dreams.
</div>
</div>
</div>
<div class="relative mb-3">
<h6 class="mb-0">
<button
class="relative border border-third bg-third/10 shadow-md shadow-third/20 rounded-lg flex items-center w-full font-semibold text-left transition-all ease-in cursor-pointer text-black/60 group"
data-collapse-target="animated-collapse-25">
<i
class="fa-solid fa-question text-xl w-14 h-14 me-2 rounded-s-lg !flex justify-center items-center bg-third/80 text-white/80"></i>
<span class="me-4">Lorem Ipsum is simply dummy text</span>
<i
class="absolute right-3 text-base text-third transition-transform fa fa-chevron-down group-open:rotate-180"></i>
</button>
</h6>
<div data-collapse="animated-collapse-25"
class="h-0 overflow-hidden transition-all duration-300 ease-in-out">
<div class="pt-4 text-sm leading-normal text-black/60">
We're not always in the position that we want to be at. We're constantly
growing. We're constantly making mistakes. We're constantly trying to
express ourselves and actualize our dreams.
</div>
</div>
</div>
<div class="grid gap-4">
<h4 class="text-lg font-medium text-fourth/80">Billing</h4>
<div class="relative mb-3">
<h6 class="mb-0">
<button
class="relative border border-fourth bg-fourth/10 shadow-md shadow-fourth/20 rounded-lg flex items-center w-full font-semibold text-left transition-all ease-in cursor-pointer text-black/60 group"
data-collapse-target="animated-collapse-26">
<i
class="fa-solid fa-question text-xl w-14 h-14 me-2 rounded-s-lg !flex justify-center items-center bg-fourth/80 text-white/80"></i>
<span class="me-4">Lorem Ipsum is simply dummy text</span>
<i
class="absolute right-3 text-base text-fourth transition-transform fa fa-chevron-down group-open:rotate-180"></i>
</button>
</h6>
<div data-collapse="animated-collapse-26"
class="h-0 overflow-hidden transition-all duration-300 ease-in-out">
<div class="pt-4 text-sm leading-normal text-black/60">
We're not always in the position that we want to be at. We're constantly
growing. We're constantly making mistakes. We're constantly trying to
express ourselves and actualize our dreams.
</div>
</div>
</div>
<div class="relative mb-3">
<h6 class="mb-0">
<button
class="relative border border-fourth bg-fourth/10 shadow-md shadow-fourth/20 rounded-lg flex items-center w-full font-semibold text-left transition-all ease-in cursor-pointer text-black/60 group"
data-collapse-target="animated-collapse-27">
<i
class="fa-solid fa-question text-xl w-14 h-14 me-2 rounded-s-lg !flex justify-center items-center bg-fourth/80 text-white/80"></i>
<span class="me-4">Lorem Ipsum is simply dummy text</span>
<i
class="absolute right-3 text-base text-fourth transition-transform fa fa-chevron-down group-open:rotate-180"></i>
</button>
</h6>
<div data-collapse="animated-collapse-27"
class="h-0 overflow-hidden transition-all duration-300 ease-in-out">
<div class="pt-4 text-sm leading-normal text-black/60">
We're not always in the position that we want to be at. We're constantly
growing. We're constantly making mistakes. We're constantly trying to
express ourselves and actualize our dreams.
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- section 5 -->
<section class="my-10">
<div class="container mx-auto">
<div class="bg-white p-8">
<div class="grid gap-8 items-center md:grid-cols-2">
<div class="grid gap-8 md:grid-cols-2">
<div class="text-center p-6 bg-fourth/10 rounded-xl shadow shadow-fourth/80">
<img src="assets/images/user/user-2.jpg" alt="dummy"
class="w-28 rounded-full mx-auto mb-4 shadow-md shadow-fourth">
<h2 class="text-lg font-bold text-fourth/80 mb-1 lg:text-xl">Jone Due</h2>
<h6 class="text-base text-black/60 font-medium">CEO</h6>
</div>
<div class="text-center p-6 bg-fourth/10 rounded-xl shadow shadow-fourth/80">
<img src="assets/images/user/user-3.jpg" alt="dummy"
class="w-28 rounded-full mx-auto mb-4 shadow-md shadow-fourth">
<h2 class="text-lg font-bold text-fourth/80 mb-1 lg:text-xl">Jone Due</h2>
<h6 class="text-base text-black/60 font-medium">CEO</h6>
</div>
<div class="text-center p-6 bg-fourth/10 rounded-xl shadow shadow-fourth/80">
<img src="assets/images/user/user-4.jpg" alt="dummy"
class="w-28 rounded-full mx-auto mb-4 shadow-md shadow-fourth">
<h2 class="text-lg font-bold text-fourth/80 mb-1 lg:text-xl">Jone Due</h2>
<h6 class="text-base text-black/60 font-medium">CEO</h6>
</div>
<div class="text-center p-6 bg-fourth/10 rounded-xl shadow shadow-fourth/80">
<img src="assets/images/user/user-5.jpg" alt="dummy"
class="w-28 rounded-full mx-auto mb-4 shadow-md shadow-fourth">
<h2 class="text-lg font-bold text-fourth/80 mb-1 lg:text-xl">Jone Due</h2>
<h6 class="text-base text-black/60 font-medium">CEO</h6>
</div>
</div>
<div>
<h4 class="text-base text-primary/60 font-medium mb-1">What about</h4>
<h2 class="font-bold text-black/80 text-xl mb-8 lg:text-2xl">Meet Our Team</h2>
<p class="text-black/40 text-base mb-4">How does FLOD work? We use a large number of servers worldwide to filter out unwanted and unusual requests to IP addresses and services.
All slave systems send their collected diagnostic data to the master server in real time.
Blacklists are created in real time from the data from the master servers.
This allows connections to be blocked at the first level in firewalls and on various operating systems.
The systems run day and night and always provide live data. </p>
<p class="text-black/40 text-base mb-4">Even though our systems serve both IPv4 and IPv6 requests, we rarely detect connection attempts via IPv6.
Nevertheless, we are constantly increasing the number of breadcrumbs we provide. </p>
</div>
</div>
</div>
</div>
</section>
<!-- footer -->
<footer class="bg-primary/60 text-center py-4">
<p class="text-white text-base">Copyright &copy; 2024 - TotalSuite</p>
</footer>
<script src="assets/js/collapse.js"></script>
<script>
async function checkIP() {
const ip = document.getElementById('ipInput').value.trim();
if (!ip) { alert("Please enter IP!"); return; }
const res = await fetch('/check/' + ip);
const data = await res.json();
document.getElementById('result').innerText = JSON.stringify(data, null, 2);
//addHistory(ip, data);
}
async function whitelistIP() {
const ip = document.getElementById('ipInput').value.trim();
if (!ip) { alert("Please enter IP!"); return; }
const res = await fetch('/whitelist', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ip})
});
const data = await res.json();
document.getElementById('result').innerText = JSON.stringify(data, null, 2);
//addHistory(ip, data);
}
/*function addHistory(ip, data) {
let history = JSON.parse(localStorage.getItem('ipHistory') || '[]');
history.unshift({ip, data, ts: new Date().toLocaleString()});
if (history.length > 10) history = history.slice(0, 10);
localStorage.setItem('ipHistory', JSON.stringify(history));
renderHistory();
}*/
/*function renderHistory() {
let history = JSON.parse(localStorage.getItem('ipHistory') || '[]');
if (history.length === 0) {
document.getElementById('history').innerText = 'Nothing checked yet';
return;
}
document.getElementById('history').innerText = history.map(e =>
e.ts + ": " + e.ip + " → blocked=" + (e.data.blocked ? "yes" : "no") +
(e.data.categories ? " [" + e.data.categories.join(", ") + "]" : "")
).join("\n");
}*/
async function loadMetrics() {
const res = await fetch('/metrics');
const text = await res.text();
const lines = text.split('\n').filter(l => l.includes('ipcheck_'));
document.getElementById('metrics').innerText = lines.join('\n') || 'No Data';
}
//renderHistory();
setInterval(loadMetrics, 3000);
loadMetrics();
</script>
</body>
</html>