Compare commits

..

40 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
193aed8580 Release 0.1.1
All checks were successful
release-tag / release-image (push) Successful in 1m34s
2025-06-15 20:55:12 +02:00
07882d26a6 Release 0.1.0 2025-06-15 20:54:28 +02:00
2ac9f6ce44 RC4
All checks were successful
release-tag / release-image (push) Successful in 1m38s
2025-06-14 22:05:16 +02:00
84486f6eca RC3
All checks were successful
release-tag / release-image (push) Successful in 1m35s
2025-06-14 11:43:52 +02:00
485a3b1034 RC2
All checks were successful
release-tag / release-image (push) Successful in 1m37s
2025-06-14 11:33:12 +02:00
224999bf65 test1 2025-06-14 11:27:07 +02:00
5b31974b34 RC1
All checks were successful
release-tag / release-image (push) Successful in 1m33s
2025-06-13 06:52:44 +02:00
63 changed files with 5243 additions and 429 deletions

View File

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

780
__main.go Normal file
View File

@@ -0,0 +1,780 @@
package main
import (
"bufio"
"context"
"encoding/binary"
"encoding/json"
"expvar"
"fmt"
"io"
"log"
"math/big"
"math/bits"
"net"
"net/http"
"net/netip"
"os"
"strconv"
"strings"
"sync"
"time"
lru "github.com/hashicorp/golang-lru/v2"
"github.com/redis/go-redis/v9"
)
var (
ctx = context.Background()
redisAddr = getenv("REDIS_ADDR", "10.10.5.249:6379")
//redisAddr = getenv("REDIS_ADDR", "localhost:6379")
redisTTL = time.Hour * 24
cacheSize = 100_000
blocklistCats = []string{"generic"}
rdb *redis.Client
ipCache *lru.Cache[string, []string]
// Metrics
hits = expvar.NewInt("cache_hits")
misses = expvar.NewInt("cache_misses")
queries = expvar.NewInt("ip_queries")
)
var (
totalBlockedIPs = expvar.NewInt("total_blocked_ips")
totalWhitelistEntries = expvar.NewInt("total_whitelist_entries")
)
func updateTotalsFromRedis() {
go func() {
blockCount := 0
iter := rdb.Scan(ctx, 0, "bl:*", 0).Iterator()
for iter.Next(ctx) {
blockCount++
}
totalBlockedIPs.Set(int64(blockCount))
whiteCount := 0
iter = rdb.Scan(ctx, 0, "wl:*", 0).Iterator()
for iter.Next(ctx) {
whiteCount++
}
totalWhitelistEntries.Set(int64(whiteCount))
}()
}
func startMetricUpdater() {
ticker := time.NewTicker(10 * time.Second)
go func() {
for {
updateTotalsFromRedis()
<-ticker.C
}
}()
}
//
//
//
type Source struct {
Category string
URL []string
}
type Config struct {
RedisAddr string
Sources []Source
TTLHours int
IsWorker bool // true ⇒ lädt Blocklisten & schreibt sie nach Redis
}
func loadConfig() Config {
// default Blocklist source
srcs := []Source{{
Category: "generic",
URL: []string{
"https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level1.netset",
"https://raw.githubusercontent.com/bitwire-it/ipblocklist/refs/heads/main/ip-list.txt",
"https://ipv64.net/blocklists/countries/ipv64_blocklist_RU.txt",
"https://ipv64.net/blocklists/countries/ipv64_blocklist_CN.txt",
},
},
}
if env := os.Getenv("BLOCKLIST_SOURCES"); env != "" {
srcs = nil
for _, spec := range strings.Split(env, ",") {
spec = strings.TrimSpace(spec)
if spec == "" {
continue
}
parts := strings.SplitN(spec, ":", 2)
if len(parts) != 2 {
continue
}
cat := strings.TrimSpace(parts[0])
raw := strings.FieldsFunc(parts[1], func(r rune) bool { return r == '|' || r == ';' })
var urls []string
for _, u := range raw {
if u = strings.TrimSpace(u); u != "" {
urls = append(urls, u)
}
}
if len(urls) > 0 {
srcs = append(srcs, Source{Category: cat, URL: urls})
}
}
}
ttl := 24
if env := os.Getenv("TTL_HOURS"); env != "" {
fmt.Sscanf(env, "%d", &ttl)
}
isWorker := strings.ToLower(os.Getenv("ROLE")) == "worker"
return Config{
//RedisAddr: getenv("REDIS_ADDR", "redis:6379"),
RedisAddr: getenv("REDIS_ADDR", "10.10.5.249:6379"),
Sources: srcs,
TTLHours: ttl,
IsWorker: isWorker,
}
}
// Alle gültigen ISO 3166-1 Alpha-2 Ländercodes (abgekürzt, reale Liste ist länger)
var allCountryCodes = []string{
"AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AR", "AT", "AU", "AZ",
"BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BN", "BO", "BR", "BS",
"BT", "BW", "BY", "BZ", "CA", "CD", "CF", "CG", "CH", "CI", "CL", "CM", "CN",
"CO", "CR", "CU", "CV", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC",
"EE", "EG", "ER", "ES", "ET", "FI", "FJ", "FM", "FR", "GA", "GB", "GD", "GE",
"GH", "GM", "GN", "GQ", "GR", "GT", "GW", "GY", "HK", "HN", "HR", "HT", "HU",
"ID", "IE", "IL", "IN", "IQ", "IR", "IS", "IT", "JM", "JO", "JP", "KE", "KG",
"KH", "KI", "KM", "KN", "KP", "KR", "KW", "KZ", "LA", "LB", "LC", "LI", "LK",
"LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MG", "MH", "MK",
"ML", "MM", "MN", "MR", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NE",
"NG", "NI", "NL", "NO", "NP", "NR", "NZ", "OM", "PA", "PE", "PG", "PH", "PK",
"PL", "PT", "PW", "PY", "QA", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD",
"SE", "SG", "SI", "SK", "SL", "SM", "SN", "SO", "SR", "ST", "SV", "SY", "SZ",
"TD", "TG", "TH", "TJ", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TZ", "UA",
"UG", "US", "UY", "UZ", "VC", "VE", "VN", "VU", "WS", "YE", "ZA", "ZM", "ZW",
}
// Hauptfunktion: gibt alle IPv4-Ranges eines Landes (CIDR) aus allen RIRs zurück
func GetIPRangesByCountry(countryCode string) ([]string, error) {
var allCIDRs []string
upperCode := strings.ToUpper(countryCode)
for _, url := range rirFiles {
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("fehler beim abrufen von %s: %w", url, err)
}
defer resp.Body.Close()
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "2") || strings.HasPrefix(line, "#") {
continue // Kommentar oder Header
}
if strings.Contains(line, "|"+upperCode+"|ipv4|") {
fields := strings.Split(line, "|")
if len(fields) < 5 {
continue
}
ipStart := fields[3]
count, _ := strconv.Atoi(fields[4])
cidrs := summarizeCIDR(ipStart, count)
allCIDRs = append(allCIDRs, cidrs...)
}
}
}
return allCIDRs, nil
}
// Hilfsfunktion: Start-IP + Anzahl → []CIDR
func summarizeCIDR(start string, count int) []string {
var cidrs []string
ip := net.ParseIP(start).To4()
startInt := ipToInt(ip)
for count > 0 {
maxSize := 32
for maxSize > 0 {
mask := 1 << uint(32-maxSize)
if startInt%uint32(mask) == 0 && mask <= count {
break
}
maxSize--
}
cidr := fmt.Sprintf("%s/%d", intToIP(startInt), maxSize)
cidrs = append(cidrs, cidr)
count -= 1 << uint(32-maxSize)
startInt += uint32(1 << uint(32-maxSize))
}
return cidrs
}
func ipToInt(ip net.IP) uint32 {
return uint32(ip[0])<<24 + uint32(ip[1])<<16 + uint32(ip[2])<<8 + uint32(ip[3])
}
func intToIP(i uint32) net.IP {
return net.IPv4(byte(i>>24), byte(i>>16), byte(i>>8), byte(i))
}
func keyBlock(cat string, p netip.Prefix) string { return "bl:" + cat + ":" + p.String() }
func LoadAllCountryPrefixesIntoRedisAndRanger(
rdb *redis.Client,
ttlHours int,
) error {
for _, countryCode := range allCountryCodes {
expiry := time.Duration(ttlHours) * time.Hour
results := make(map[string][]netip.Prefix)
fmt.Printf("💡 Loading %s...\n", countryCode)
cidrs, err := GetIPRangesByCountry(countryCode)
if err != nil {
log.Printf("Error at %s: %v", countryCode, err)
}
fmt.Println("✅ Got " + strconv.Itoa(len(cidrs)) + " Ranges for Country " + countryCode)
var validPrefixes []netip.Prefix
for _, c := range cidrs {
prefix, err := netip.ParsePrefix(c)
if err != nil {
log.Printf("CIDR invalid [%s]: %v", c, err)
continue
}
validPrefixes = append(validPrefixes, prefix)
}
fmt.Println("✅ Got " + strconv.Itoa(len(validPrefixes)) + " valid Prefixes for Country " + countryCode)
if len(validPrefixes) > 0 {
results[countryCode] = validPrefixes
}
// Nach Verarbeitung: alles in Ranger + Redis eintragen
for code, prefixes := range results {
for _, p := range prefixes {
key := keyBlock(code, p)
if err := rdb.Set(ctx, key, "1", expiry).Err(); err != nil {
log.Printf("Redis-Error at %s: %v", key, err)
}
}
fmt.Println("✅ Import Subset " + strconv.Itoa(len(prefixes)) + " Entries")
}
fmt.Println("✅ Import done!")
fmt.Println("--------------------------------------------------")
}
return nil
}
func syncLoop(ctx context.Context, cfg Config, rdb *redis.Client) {
fmt.Println("💡 Loading Lists...")
if err := syncOnce(ctx, cfg, rdb); err != nil {
log.Println("initial sync:", err)
}
fmt.Println("✅ Loading Lists Done.")
ticker := time.NewTicker(30 * time.Minute)
for {
select {
case <-ticker.C:
fmt.Println("💡 Loading Lists Timer...")
if err := syncOnce(ctx, cfg, rdb); err != nil {
log.Println("sync loop:", err)
}
fmt.Println("✅ Loading Lists Timer Done.")
case <-ctx.Done():
ticker.Stop()
return
}
}
}
func syncOnce(ctx context.Context, cfg Config, rdb *redis.Client) error {
expiry := time.Duration(cfg.TTLHours) * time.Hour
newBlocks := make(map[string]map[netip.Prefix]struct{})
for _, src := range cfg.Sources {
for _, url := range src.URL {
fmt.Println("💡 Loading List " + src.Category + " : " + url)
if err := fetchList(ctx, url, func(p netip.Prefix) {
if _, ok := newBlocks[src.Category]; !ok {
newBlocks[src.Category] = map[netip.Prefix]struct{}{}
}
newBlocks[src.Category][p] = struct{}{}
_ = rdb.Set(ctx, keyBlock(src.Category, p), "1", expiry).Err()
}); err != nil {
fmt.Println("❌ Fail.")
return err
}
fmt.Println("✅ Done.")
}
}
return nil
}
func fetchList(ctx context.Context, url string, cb func(netip.Prefix)) error {
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("%s -> %s", url, resp.Status)
}
return parseStream(resp.Body, cb)
}
func parseStream(r io.Reader, cb func(netip.Prefix)) error {
s := bufio.NewScanner(r)
for s.Scan() {
line := strings.TrimSpace(s.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if p, err := netip.ParsePrefix(line); err == nil {
cb(p)
continue
}
if addr, err := netip.ParseAddr(line); err == nil {
plen := 32
if addr.Is6() {
plen = 128
}
cb(netip.PrefixFrom(addr, plen))
}
}
return s.Err()
}
// --------------------------------------------
// INIT + MAIN
// --------------------------------------------
func main() {
if getenv("IMPORTER", "0") == "1" {
//Hier alles doof. selbe funktion wie unten. muss durch individuallisten ersetzt werden...
cfg := loadConfig()
rdb = redis.NewClient(&redis.Options{Addr: redisAddr})
/*if err := LoadAllCountryPrefixesIntoRedisAndRanger(rdb, cfg.TTLHours); err != nil {
log.Fatalf("Fehler beim Laden aller Länderranges: %v", err)
}*/
syncLoop(ctx, cfg, rdb)
log.Println("🚀 Import erfolgreich!")
} else {
var err error
// Redis client
rdb = redis.NewClient(&redis.Options{Addr: redisAddr})
if err := rdb.Ping(ctx).Err(); err != nil {
log.Fatalf("redis: %v", err)
}
// LRU cache
ipCache, err = lru.New[string, []string](cacheSize)
if err != nil {
log.Fatalf("cache init: %v", err)
}
startMetricUpdater()
// Admin load all blocklists (on demand or scheduled)
go func() {
if getenv("IMPORT_RIRS", "0") == "1" {
log.Println("Lade IP-Ranges aus RIRs...")
if err := importRIRDataToRedis(); err != nil {
log.Fatalf("import error: %v", err)
}
log.Println("✅ Import abgeschlossen.")
}
}()
// Routes
http.HandleFunc("/check/", handleCheck)
http.HandleFunc("/whitelist", handleWhitelist)
http.HandleFunc("/info", handleInfo)
http.Handle("/debug/vars", http.DefaultServeMux)
log.Println("🚀 Server läuft auf :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
}
func getenv(k, fallback string) string {
if v := os.Getenv(k); v != "" {
return v
}
return fallback
}
// --------------------------------------------
// IP CHECK API
// --------------------------------------------
func handleCheck(w http.ResponseWriter, r *http.Request) {
ipStr := strings.TrimPrefix(r.URL.Path, "/check/")
addr, err := netip.ParseAddr(ipStr)
if err != nil {
http.Error(w, "invalid IP", 400)
return
}
cats := blocklistCats
if q := r.URL.Query().Get("cats"); q != "" {
cats = strings.Split(q, ",")
}
queries.Add(1)
blockedCats, err := checkIP(addr, cats)
if err != nil {
http.Error(w, "lookup error", 500)
return
}
fmt.Println("----------------")
writeJSON(w, map[string]any{
"ip": ipStr,
"blocked": len(blockedCats) > 0,
"categories": blockedCats,
})
}
// liefert alle möglichen Präfixe dieser IP, beginnend beim längsten (/32 oder /128)
func supernets(ip netip.Addr) []string {
if ip.Is4() {
fmt.Println("💡 DEBUG: supernets | Is4")
a := ip.As4() // Kopie addressierbar machen
u := binary.BigEndian.Uint32(a[:]) // jetzt darf man slicen
fmt.Println("💡 DEBUG: supernets | a + u", a, u)
supers := make([]string, 33) // /32 … /0
for bits := 32; bits >= 0; bits-- {
mask := uint32(0xffffffff) << (32 - bits)
n := u & mask
addr := netip.AddrFrom4([4]byte{
byte(n >> 24), byte(n >> 16), byte(n >> 8), byte(n),
})
supers[32-bits] = fmt.Sprintf("%s/%d", addr, bits)
}
fmt.Println("💡 DEBUG: supernets | supers", supers)
return supers
}
a := ip.As16() // Kopie addressierbar
supers := make([]string, 129) // /128 … /0
for bits := 128; bits >= 0; bits-- {
b := a // Wert-Kopie für Modifikation
// vollständige Bytes auf 0 setzen
full := (128 - bits) / 8
for i := 0; i < full; i++ {
b[15-i] = 0
}
// Restbits maskieren
rem := (128 - bits) % 8
if rem != 0 {
b[15-full] &= 0xFF << rem
}
addr := netip.AddrFrom16(b)
supers[128-bits] = fmt.Sprintf("%s/%d", addr, bits)
}
fmt.Println("Supernets-v6", supers)
return supers
}
func checkIP(ip netip.Addr, cats []string) ([]string, error) {
// 1) Cache-Treffer?
if res, ok := ipCache.Get(ip.String()); ok {
fmt.Println("💡 DEBUG: Cache-Hit")
hits.Add(1)
return res, nil
}
// 2) alle Supernetze der IP (≤32 bzw. ≤128 Stück)
supers := supernets(ip)
fmt.Println("💡 DEBUG: checkIP | supers |", supers)
fmt.Println("💡 DEBUG: checkIP | ip |", ip)
fmt.Println("💡 DEBUG: checkIP | cats |", cats)
// 3) Pipeline jeweils *eine* EXISTS-Abfrage pro Kategorie
pipe := rdb.Pipeline()
existsCmds := make([]*redis.IntCmd, len(cats))
for i, cat := range cats {
keys := make([]string, len(supers))
for j, pfx := range supers {
keys[j] = "bl:" + cat + ":" + pfx
}
fmt.Println("💡 DEBUG: checkIP | keys |", keys)
existsCmds[i] = pipe.Exists(ctx, keys...)
}
if _, err := pipe.Exec(ctx); err != nil && err != redis.Nil {
return nil, err
}
// 4) Ergebnis auswerten
matches := make([]string, 0, len(cats))
for i, cat := range cats {
if existsCmds[i].Val() > 0 {
matches = append(matches, cat)
fmt.Println("💡 DEBUG: checkIP | matches:cat |", cat)
}
}
fmt.Println("💡 DEBUG: checkIP | matches |", matches)
// 5) Cache befüllen und zurück
misses.Add(1)
ipCache.Add(ip.String(), matches)
return matches, nil
}
// --------------------------------------------
// WHITELIST API (optional extension)
// --------------------------------------------
func handleWhitelist(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var body struct {
IP string `json:"ip"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "bad request", 400)
return
}
addr, err := netip.ParseAddr(body.IP)
if err != nil {
http.Error(w, "invalid IP", 400)
return
}
// Add to whitelist (Redis key like wl:<ip>)
if err := rdb.Set(ctx, "wl:"+addr.String(), "1", 0).Err(); err != nil {
http.Error(w, "redis error", 500)
return
}
ipCache.Add(addr.String(), nil)
writeJSON(w, map[string]string{"status": "whitelisted"})
}
// --------------------------------------------
// ADMIN INFO
// --------------------------------------------
func handleInfo(w http.ResponseWriter, _ *http.Request) {
stats := map[string]any{
"cache_size": ipCache.Len(),
"ttl_hours": redisTTL.Hours(),
"redis": redisAddr,
}
writeJSON(w, stats)
}
// --------------------------------------------
// UTIL
// --------------------------------------------
func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(v)
}
// --------------------------------------------
// RIR DATA IMPORT (ALL COUNTRIES)
// --------------------------------------------
var rirFiles = []string{
"https://ftp.ripe.net/pub/stats/ripencc/delegated-ripencc-latest",
"https://ftp.apnic.net/stats/apnic/delegated-apnic-latest",
"https://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest",
"https://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-latest",
"https://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-extended-latest",
}
func importRIRDataToRedis() error {
wg := sync.WaitGroup{}
sem := make(chan struct{}, 5)
for _, url := range rirFiles {
wg.Add(1)
sem <- struct{}{}
go func(url string) {
defer wg.Done()
defer func() { <-sem }()
fmt.Println("Start: ", url)
if err := fetchAndStore(url); err != nil {
log.Printf("❌ Fehler bei %s: %v", url, err)
}
fmt.Println("Done: ", url)
}(url)
}
wg.Wait()
return nil
}
func fetchAndStore(url string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "#") || !strings.Contains(line, "|ipv") {
continue
}
fields := strings.Split(line, "|")
if len(fields) < 7 {
continue
}
country := strings.ToLower(fields[1])
ipType := fields[2]
start := fields[3]
count := fields[4]
if ipType != "ipv4" && ipType != "ipv6" {
continue
}
if start == "24.152.36.0" {
fmt.Printf("💡 Testing summarizeIPv4CIDRs(%s, %s)\n", start, count)
num, _ := strconv.ParseUint(count, 10, 64)
for _, cidr := range summarizeCIDRs(start, num) {
fmt.Println(" →", cidr)
}
}
//cidrList := summarizeToCIDRs(start, count, ipType)
numIPs, _ := strconv.ParseUint(count, 10, 64)
cidrList := summarizeCIDRs(start, numIPs)
//log.Printf("[%s] %s/%s (%s) → %d Netze", strings.ToUpper(country), start, count, ipType, len(cidrList))
for _, cidr := range cidrList {
prefix, err := netip.ParsePrefix(cidr)
if err != nil {
continue
}
key := "bl:" + country + ":" + prefix.String()
//fmt.Println(key)
_ = rdb.Set(ctx, key, "1", redisTTL).Err()
}
}
return scanner.Err()
}
// --------------------------------------------
// IP RANGE SUMMARIZER
// --------------------------------------------
func summarizeCIDRs(startIP string, count uint64) []string {
var result []string
if count == 0 {
return result
}
ip := net.ParseIP(startIP)
if ip == nil {
return result
}
// IPv4-Pfad ---------------------------------------------------------------
if v4 := ip.To4(); v4 != nil {
start := ip4ToUint(v4)
end := start + uint32(count) - 1
for start <= end {
prefix := 32 - uint32(bits.TrailingZeros32(start))
for (start + (1 << (32 - prefix)) - 1) > end {
prefix++
}
result = append(result,
fmt.Sprintf("%s/%d", uintToIP4(start), prefix))
start += 1 << (32 - prefix)
}
return result
}
// IPv6-Pfad ---------------------------------------------------------------
startBig := ip6ToBig(ip) // Startadresse
endBig := new(big.Int).Add(startBig, // Endadresse
new(big.Int).Sub(new(big.Int).SetUint64(count), big.NewInt(1)))
for startBig.Cmp(endBig) <= 0 {
// größter Block, der am Start ausgerichtet ist
prefix := 128 - trailingZeros128(bigToIP6(startBig))
// so lange verkleinern, bis Block in Fenster passt
for {
blockSize := new(big.Int).Lsh(big.NewInt(1), uint(128-prefix))
blockEnd := new(big.Int).Add(startBig,
new(big.Int).Sub(blockSize, big.NewInt(1)))
if blockEnd.Cmp(endBig) <= 0 {
break
}
prefix++
}
result = append(result,
fmt.Sprintf("%s/%d", bigToIP6(startBig), prefix))
// zum nächsten Subnetz springen
step := new(big.Int).Lsh(big.NewInt(1), uint(128-prefix))
startBig = new(big.Int).Add(startBig, step)
}
return result
}
/* ---------- Hilfsfunktionen IPv4 ---------- */
func ip4ToUint(ip net.IP) uint32 {
return uint32(ip[0])<<24 | uint32(ip[1])<<16 | uint32(ip[2])<<8 | uint32(ip[3])
}
func uintToIP4(v uint32) net.IP {
return net.IPv4(byte(v>>24), byte(v>>16), byte(v>>8), byte(v))
}
/* ---------- Hilfsfunktionen IPv6 ---------- */
func ip6ToBig(ip net.IP) *big.Int {
return new(big.Int).SetBytes(ip.To16()) // garantiert 16 Byte
}
func bigToIP6(v *big.Int) net.IP {
b := v.Bytes()
if len(b) < 16 { // von links auf 16 Byte auffüllen
pad := make([]byte, 16-len(b))
b = append(pad, b...)
}
return net.IP(b)
}
// Anzahl der Null-Bits am wenigst-signifikanten Ende (LSB) eines IPv6-Werts
func trailingZeros128(ip net.IP) int {
b := ip.To16()
tz := 0
for i := 15; i >= 0; i-- { // letzte Byte zuerst (LSB)
if b[i] == 0 {
tz += 8
} else {
tz += bits.TrailingZeros8(b[i])
break
}
}
return tz
}

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

17
go.mod
View File

@@ -1,10 +1,21 @@
module git.send.nrw/sendnrw/flod
go 1.24.3
go 1.24.4
require (
github.com/prometheus/client_golang v1.22.0
github.com/redis/go-redis/v9 v9.10.0
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/redis/go-redis/v9 v9.10.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/yl2chen/cidranger v1.0.2 // indirect
golang.org/x/sys v0.30.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
)

43
go.sum
View File

@@ -1,8 +1,47 @@
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/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/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/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
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/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/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
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/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/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/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 @@
{}

1467
main.go

File diff suppressed because it is too large Load Diff

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>