This commit is contained in:
67
.gitea/workflows/registry.yml
Normal file
67
.gitea/workflows/registry.yml
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
name: release-tag
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'main'
|
||||||
|
jobs:
|
||||||
|
release-image:
|
||||||
|
runs-on: ubuntu-fast
|
||||||
|
env:
|
||||||
|
DOCKER_ORG: ${{ vars.DOCKER_ORG }}
|
||||||
|
DOCKER_LATEST: latest
|
||||||
|
RUNNER_TOOL_CACHE: /toolcache
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: Set up Docker BuildX
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
with: # replace it with your local IP
|
||||||
|
config-inline: |
|
||||||
|
[registry."${{ vars.DOCKER_REGISTRY }}"]
|
||||||
|
http = true
|
||||||
|
insecure = true
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ${{ vars.DOCKER_REGISTRY }} # replace it with your local IP
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Get Meta
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
|
||||||
|
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
build-args: |
|
||||||
|
CONTENT_REPO=https://git.send.nrw/b1tsblog/blogcontent.git
|
||||||
|
CONTENT_REF=main
|
||||||
|
platforms: |
|
||||||
|
linux/amd64
|
||||||
|
push: true
|
||||||
|
tags: | # replace it with your local IP and tags
|
||||||
|
${{ vars.DOCKER_REGISTRY }}/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
|
||||||
|
${{ vars.DOCKER_REGISTRY }}/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}
|
||||||
|
- name: Build and push StarCitizen
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
build-args: |
|
||||||
|
CONTENT_REPO=https://git.send.nrw/b1tsblog/sccontent.git
|
||||||
|
CONTENT_REF=main
|
||||||
|
platforms: |
|
||||||
|
linux/amd64
|
||||||
|
push: true
|
||||||
|
tags: | # replace it with your local IP and tags
|
||||||
|
${{ vars.DOCKER_REGISTRY }}/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:sc-${{ env.DOCKER_LATEST }}
|
26
Dockerfile
Normal file
26
Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# -------- Dockerfile (Multi-Stage Build) --------
|
||||||
|
# 1. Builder-Stage
|
||||||
|
FROM golang:1.24-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY go.* ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /bin/ipblock-api
|
||||||
|
|
||||||
|
# 2. Runtime-Stage
|
||||||
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
# HTTPS-Callouts in Alpine brauchen ca-certificates
|
||||||
|
RUN apk add --no-cache ca-certificates
|
||||||
|
|
||||||
|
COPY --from=builder /bin/ipblock-api /bin/ipblock-api
|
||||||
|
|
||||||
|
# Default listens on :8080 – siehe main.go
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Environment defaults; können per compose überschrieben werden
|
||||||
|
ENV REDIS_ADDR=redis:6379 \
|
||||||
|
TTL_HOURS=720
|
||||||
|
|
||||||
|
ENTRYPOINT [\"/bin/ipblock-api\"]
|
30
compose.yml
Normal file
30
compose.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# -------- docker-compose.yml --------
|
||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
build: .
|
||||||
|
container_name: ipblock-api
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
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
|
||||||
|
# Redis-Adresse schon per Docker-Netzwerk korrekt:
|
||||||
|
REDIS_ADDR: redis:6379
|
||||||
|
TTL_HOURS: "720"
|
||||||
|
ports:
|
||||||
|
- "8080:8080" # <host>:<container>
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: ipblock-redis
|
||||||
|
command: ["redis-server", "--save", "", "--appendonly", "no"] # reine In-Memory-Instanz
|
||||||
|
#volumes:
|
||||||
|
#- redis-data:/data # falls du doch Persistence willst
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
#volumes:
|
||||||
|
#redis-data:
|
9
go.mod
Normal file
9
go.mod
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
module git.send.nrw/sendnrw/flod
|
||||||
|
|
||||||
|
go 1.24.3
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/redis/go-redis/v9 v9.10.0 // indirect
|
||||||
|
)
|
6
go.sum
Normal file
6
go.sum
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
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/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/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=
|
329
main.go
Normal file
329
main.go
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// CONFIGURATION
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Source struct {
|
||||||
|
Category string // e.g. "spam", "tor", "malware"
|
||||||
|
URL []string // one or many URLs that belong to the same category
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
RedisAddr string
|
||||||
|
Sources []Source // each Source now groups URLs per category
|
||||||
|
TTLHours int // Redis TTL for block entries
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfig() Config {
|
||||||
|
// --- default one‑category fallback --------------------------------------
|
||||||
|
srcs := []Source{{
|
||||||
|
Category: "generic",
|
||||||
|
URL: []string{"https://ipv64.net/blocklists/ipv64_blocklist_firehole_l1.txt"},
|
||||||
|
}}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ENV syntax supporting multiple URLs per category:
|
||||||
|
|
||||||
|
BLOCKLIST_SOURCES="spam:https://a.net|https://b.net,tor:https://c.net;https://d.net"
|
||||||
|
|
||||||
|
– categories separated by comma
|
||||||
|
– URLs inside category separated by | or ;
|
||||||
|
*/
|
||||||
|
|
||||||
|
if env := os.Getenv("BLOCKLIST_SOURCES"); env != "" {
|
||||||
|
srcs = nil // override default
|
||||||
|
for _, spec := range strings.Split(env, ",") {
|
||||||
|
spec = strings.TrimSpace(spec)
|
||||||
|
if spec == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(spec, ":", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue // malformed fragment
|
||||||
|
}
|
||||||
|
cat := strings.TrimSpace(parts[0])
|
||||||
|
rawURLs := strings.FieldsFunc(parts[1], func(r rune) bool { return r == '|' || r == ';' })
|
||||||
|
var urls []string
|
||||||
|
for _, u := range rawURLs {
|
||||||
|
if u = strings.TrimSpace(u); u != "" {
|
||||||
|
urls = append(urls, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(urls) > 0 {
|
||||||
|
srcs = append(srcs, Source{Category: cat, URL: urls})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ttl := 720 // 30 days
|
||||||
|
if env := os.Getenv("TTL_HOURS"); env != "" {
|
||||||
|
fmt.Sscanf(env, "%d", &ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Config{
|
||||||
|
RedisAddr: getenv("REDIS_ADDR", "localhost:6379"),
|
||||||
|
Sources: srcs,
|
||||||
|
TTLHours: ttl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getenv(k, def string) string {
|
||||||
|
if v := os.Getenv(k); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// REDIS KEY HELPERS
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func keyBlock(cat string, p netip.Prefix) string { return "bl:" + cat + ":" + p.String() }
|
||||||
|
func keyWhite(a netip.Addr) string { return "wl:" + a.String() }
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// IN‑MEMORY RANGER – per‑category CIDR map
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Ranger struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
blocks map[string]map[netip.Prefix]struct{} // cat → set(prefix)
|
||||||
|
whites map[netip.Addr]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRanger() *Ranger {
|
||||||
|
return &Ranger{
|
||||||
|
blocks: make(map[string]map[netip.Prefix]struct{}),
|
||||||
|
whites: make(map[netip.Addr]struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Ranger) resetBlocks(m map[string]map[netip.Prefix]struct{}) {
|
||||||
|
r.mu.Lock()
|
||||||
|
r.blocks = m
|
||||||
|
r.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Ranger) addWhite(a netip.Addr) {
|
||||||
|
r.mu.Lock()
|
||||||
|
r.whites[a] = struct{}{}
|
||||||
|
r.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// blockedInCats – returns slice of categories in which IP is blocked
|
||||||
|
func (r *Ranger) blockedInCats(a netip.Addr, cats []string) []string {
|
||||||
|
r.mu.RLock()
|
||||||
|
defer r.mu.RUnlock()
|
||||||
|
|
||||||
|
if _, ok := r.whites[a]; ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cats) == 0 {
|
||||||
|
for c := range r.blocks {
|
||||||
|
cats = append(cats, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var res []string
|
||||||
|
for _, cat := range cats {
|
||||||
|
if m, ok := r.blocks[cat]; ok {
|
||||||
|
for p := range m {
|
||||||
|
if p.Contains(a) {
|
||||||
|
res = append(res, cat)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(res)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// SYNC WORKER – fetch lists → Redis + Ranger
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func syncOnce(ctx context.Context, cfg Config, rdb *redis.Client, ranger *Ranger) 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 {
|
||||||
|
if err := processURL(ctx, url, func(p netip.Prefix) {
|
||||||
|
if _, ok := newBlocks[src.Category]; !ok {
|
||||||
|
newBlocks[src.Category] = make(map[netip.Prefix]struct{})
|
||||||
|
}
|
||||||
|
newBlocks[src.Category][p] = struct{}{}
|
||||||
|
_ = rdb.Set(ctx, keyBlock(src.Category, p), "1", expiry).Err()
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ranger.resetBlocks(newBlocks)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func processURL(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// HTTP SERVER
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
cfg Config
|
||||||
|
ranger *Ranger
|
||||||
|
rdb *redis.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) routes() http.Handler {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/check/", s.handleCheck) // GET /check/<ip>?cats=spam,tor
|
||||||
|
mux.HandleFunc("/whitelist", s.handleAddWhite) // POST {"ip":"1.2.3.4"}
|
||||||
|
mux.HandleFunc("/categories", s.handleCats) // GET all categories
|
||||||
|
return mux
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) 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, "bad ip", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var cats []string
|
||||||
|
if q := strings.TrimSpace(r.URL.Query().Get("cats")); q != "" {
|
||||||
|
cats = strings.Split(q, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
blocked := s.ranger.blockedInCats(addr, cats)
|
||||||
|
writeJSON(w, map[string]any{
|
||||||
|
"ip": ipStr,
|
||||||
|
"blocked": len(blocked) > 0,
|
||||||
|
"categories": blocked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleAddWhite(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "POST only", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body struct {
|
||||||
|
IP string `json:"ip"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
http.Error(w, "bad json", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addr, err := netip.ParseAddr(strings.TrimSpace(body.IP))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "bad ip", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := s.rdb.Set(r.Context(), keyWhite(addr), "1", 0).Err(); err != nil {
|
||||||
|
http.Error(w, "redis", 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.ranger.addWhite(addr)
|
||||||
|
writeJSON(w, map[string]string{"status": "whitelisted"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleCats(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
s.ranger.mu.RLock()
|
||||||
|
cats := make([]string, 0, len(s.ranger.blocks))
|
||||||
|
for c := range s.ranger.blocks {
|
||||||
|
cats = append(cats, c)
|
||||||
|
}
|
||||||
|
s.ranger.mu.RUnlock()
|
||||||
|
sort.Strings(cats)
|
||||||
|
writeJSON(w, map[string]any{"categories": cats})
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeJSON(w http.ResponseWriter, v any) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// MAIN
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := loadConfig()
|
||||||
|
|
||||||
|
rdb := redis.NewClient(&redis.Options{Addr: cfg.RedisAddr})
|
||||||
|
ctx := context.Background()
|
||||||
|
if err := rdb.Ping(ctx).Err(); err != nil {
|
||||||
|
log.Fatalf("redis: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ranger := newRanger()
|
||||||
|
if err := syncOnce(ctx, cfg, rdb, ranger); err != nil {
|
||||||
|
log.Println("initial sync:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(6 * time.Hour)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
if err := syncOnce(ctx, cfg, rdb, ranger); err != nil {
|
||||||
|
log.Println("sync:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
srv := &Server{cfg: cfg, ranger: ranger, rdb: rdb}
|
||||||
|
log.Println("listening on :8080")
|
||||||
|
if err := http.ListenAndServe(":8080", srv.routes()); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user