16 Commits

Author SHA1 Message Date
Ilya Chernishev
2e9b307194 Merge pull request #1 from shinyzero00/master
All checks were successful
build / build (push) Successful in 2m25s
refactoring pr by shinyzero00
2026-02-15 13:17:01 +03:00
Ilya Chernishev
726594a712 Change return value to nil on successful IP block 2026-02-15 13:13:26 +03:00
Ilya Chernishev
b27038a59c Execute SQL statement to create table in database 2026-02-15 13:08:40 +03:00
Ilya Chernishev
72025dab7d Remove comment about potential failure in encoding
Removed commented-out question regarding error handling.
2026-02-15 12:59:20 +03:00
Ilya Chernishev
dd131477e2 fix ST1005 2026-02-15 12:51:18 +03:00
Ilya Chernishev
670aec449a fix ST1005 staticcheck 2026-02-15 12:49:57 +03:00
zero@thinky
fc37e641be refactor(internal/config): use CutSuffix 2026-02-15 04:56:22 +03:00
zero@thinky
361de03208 refactor(cmd/fw): wtf is that error handling 2026-02-15 04:56:22 +03:00
zero@thinky
a2268fda5d fix(cmd/fw): why to fucking log when it is printed by the only caller 2026-02-15 04:56:22 +03:00
zero@thinky
9dc0b6002e refactor(internal/config): error handling 2026-02-15 04:56:22 +03:00
zero@thinky
4953be3ef6 refactor(internal/storage/RequestWriter/WriteReq): wtf is that error handling 2026-02-15 04:56:22 +03:00
zero@thinky
c386a2d6bc refactor(internal/storage/RequestWriter): deduplicate dsn 2026-02-15 04:54:38 +03:00
zero@thinky
dea03a6f70 refactor(*): what the fuck is that naming 2026-02-15 04:54:38 +03:00
zero@thinky
11f755c03c style(internal/storage/BanWriter): rm extra newline 2026-02-15 04:54:38 +03:00
zero@thinky
1c7a1c1778 refactor(internal/storage/BanWriter): deduplicate dsn 2026-02-15 04:54:38 +03:00
zero@thinky
411574cabe refactor(internal/storage): generalization and deduplication 2026-02-15 04:28:34 +03:00
8 changed files with 199 additions and 189 deletions

View File

@@ -16,13 +16,14 @@ var (
port int
protocol string
)
var UnbanCmd = &cobra.Command{
Use: "unban",
Short: "Unban IP",
Run: func(cmd *cobra.Command, args []string) {
err := func() error {
if len(args) == 0 {
fmt.Println("IP can't be empty")
os.Exit(1)
return fmt.Errorf("IP can't be empty")
}
if ttl_fw == "" {
ttl_fw = "1y"
@@ -30,39 +31,38 @@ var UnbanCmd = &cobra.Command{
ip := args[0]
db, err := storage.NewBanWriter()
if err != nil {
fmt.Println(err)
os.Exit(1)
return err
}
cfg, err := config.LoadConfig()
if err != nil {
fmt.Println(err)
os.Exit(1)
return err
}
fw := cfg.Firewall.Name
b := blocker.GetBlocker(fw, cfg.Firewall.Config)
if ip == "" {
fmt.Println("IP can't be empty")
os.Exit(1)
return fmt.Errorf("IP can't be empty")
}
if net.ParseIP(ip) == nil {
fmt.Println("Invalid IP")
os.Exit(1)
return fmt.Errorf("invalid IP")
}
if err != nil {
fmt.Println(err)
os.Exit(1)
return err
}
err = b.Unban(ip)
if err != nil {
fmt.Println(err)
os.Exit(1)
return err
}
err = db.RemoveBan(ip)
if err != nil {
return err
}
fmt.Println("IP unblocked successfully!")
return nil
}()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println("IP unblocked successfully!")
},
}
@@ -70,9 +70,9 @@ var BanCmd = &cobra.Command{
Use: "ban",
Short: "Ban IP",
Run: func(cmd *cobra.Command, args []string) {
err := func() error {
if len(args) == 0 {
fmt.Println("IP can't be empty")
os.Exit(1)
return fmt.Errorf("IP can't be empty")
}
if ttl_fw == "" {
ttl_fw = "1y"
@@ -80,39 +80,38 @@ var BanCmd = &cobra.Command{
ip := args[0]
db, err := storage.NewBanWriter()
if err != nil {
fmt.Println(err)
os.Exit(1)
return err
}
cfg, err := config.LoadConfig()
if err != nil {
fmt.Println(err)
os.Exit(1)
return err
}
fw := cfg.Firewall.Name
b := blocker.GetBlocker(fw, cfg.Firewall.Config)
if ip == "" {
fmt.Println("IP can't be empty")
os.Exit(1)
return fmt.Errorf("IP can't be empty")
}
if net.ParseIP(ip) == nil {
fmt.Println("Invalid IP")
os.Exit(1)
return fmt.Errorf("invalid IP")
}
if err != nil {
fmt.Println(err)
os.Exit(1)
return err
}
err = b.Ban(ip)
if err != nil {
fmt.Println(err)
os.Exit(1)
return err
}
err = db.AddBan(ip, ttl_fw, "manual ban")
if err != nil {
return err
}
fmt.Println("IP blocked successfully!")
return nil
}()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println("IP blocked successfully!")
},
}

View File

@@ -94,7 +94,6 @@ func (f *Firewalld) PortClose(port int, protocol string) error {
// #nosec G204 - handle is extracted from nftables output and validated
if port >= 0 && port <= 65535 {
if protocol != "tcp" && protocol != "udp" {
f.logger.Error("invalid protocol")
return fmt.Errorf("invalid protocol")
}
s := strconv.Itoa(port)
@@ -106,13 +105,11 @@ func (f *Firewalld) PortClose(port int, protocol string) error {
)
output, err := cmd.CombinedOutput()
if err != nil {
f.logger.Error(err.Error())
return err
}
f.logger.Info("Remove port " + s + " " + string(output))
output, err = exec.Command("firewall-cmd", "--reload").CombinedOutput()
if err != nil {
f.logger.Error(err.Error())
return err
}
f.logger.Info("Reload " + string(output))

View File

@@ -1,6 +1,7 @@
package config
import (
"errors"
"fmt"
"os"
"strconv"
@@ -57,13 +58,9 @@ func NewRule(
return err
}
defer func() {
err = file.Close()
if err != nil {
fmt.Println(err)
}
err = errors.Join(err, file.Close())
}()
cfg := Rules{Rules: r}
err = toml.NewEncoder(file).Encode(cfg)
if err != nil {
return err
@@ -126,24 +123,24 @@ func EditRule(Name string, ServiceName string, Path string, Status string, Metho
}
func ParseDurationWithYears(s string) (time.Duration, error) {
if strings.HasSuffix(s, "y") {
years, err := strconv.Atoi(strings.TrimSuffix(s, "y"))
if ss, ok := strings.CutSuffix(s, "y"); ok {
years, err := strconv.Atoi(ss)
if err != nil {
return 0, err
}
return time.Duration(years) * 365 * 24 * time.Hour, nil
}
if strings.HasSuffix(s, "M") {
months, err := strconv.Atoi(strings.TrimSuffix(s, "M"))
if ss, ok := strings.CutSuffix(s, "M"); ok {
months, err := strconv.Atoi(ss)
if err != nil {
return 0, err
}
return time.Duration(months) * 30 * 24 * time.Hour, nil
}
if strings.HasSuffix(s, "d") {
days, err := strconv.Atoi(strings.TrimSuffix(s, "d"))
if ss, ok := strings.CutSuffix(s, "d"); ok {
days, err := strconv.Atoi(ss)
if err != nil {
return 0, err
}

View File

@@ -21,7 +21,7 @@ type BanWriter struct {
func NewBanWriter() (*BanWriter, error) {
db, err := sql.Open(
"sqlite",
"/var/lib/banforge/bans.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(30000)&_pragma=synchronous(NORMAL)",
buildSqliteDsn(banDBPath, pragmas),
)
if err != nil {
return nil, err
@@ -175,7 +175,6 @@ func (d *BanReader) IsBanned(ip string) (bool, error) {
}
func (d *BanReader) BanList() error {
var count int
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)

View File

@@ -2,55 +2,60 @@ package storage
import (
"database/sql"
"errors"
"fmt"
"strings"
_ "modernc.org/sqlite"
)
func CreateTables() error {
const (
DBDir = "/var/lib/banforge/"
ReqDBPath = DBDir + "requests.db"
banDBPath = DBDir + "bans.db"
)
var pragmas = map[string]string{
`journal_mode`: `wal`,
`synchronous`: `normal`,
`busy_timeout`: `30000`,
// also consider these
// `temp_store`: `memory`,
// `cache_size`: `1000000000`,
}
func buildSqliteDsn(path string, pragmas map[string]string) string {
pragmastrs := make([]string, len(pragmas))
i := 0
for k, v := range pragmas {
pragmastrs[i] = (fmt.Sprintf(`pragma=%s(%s)`, k, v))
i++
}
return path + "?" + "mode=rwc&" + strings.Join(pragmastrs, "&")
}
func initDB(dsn, sqlstr string) (err error) {
db, err := sql.Open("sqlite", dsn)
if err != nil {
return fmt.Errorf("failed to open %q: %w", dsn, err)
}
defer func() {
closeErr := db.Close()
if closeErr != nil {
err = errors.Join(err, fmt.Errorf("failed to close %q: %w", dsn, closeErr))
}
}()
_, err = db.Exec(sqlstr)
if err != nil {
return fmt.Errorf("failed to create table: %w", err)
}
return err
}
func CreateTables() (err error) {
// Requests DB
db_r, err := sql.Open("sqlite",
"/var/lib/banforge/requests.db?"+
"mode=rwc&"+
"_pragma=journal_mode(WAL)&"+
"_pragma=busy_timeout(30000)&"+
"_pragma=synchronous(NORMAL)")
if err != nil {
return fmt.Errorf("failed to open requests db: %w", err)
}
defer func() {
err = db_r.Close()
if err != nil {
fmt.Println(err)
}
}()
err1 := initDB(buildSqliteDsn(ReqDBPath, pragmas), CreateRequestsTable)
err2 := initDB(buildSqliteDsn(banDBPath, pragmas), CreateBansTable)
_, err = db_r.Exec(CreateRequestsTable)
if err != nil {
return fmt.Errorf("failed to create requests table: %w", err)
}
// Bans DB
db_b, err := sql.Open("sqlite",
"/var/lib/banforge/bans.db?"+
"mode=rwc&"+
"_pragma=journal_mode(WAL)&"+
"_pragma=busy_timeout(30000)&"+
"_pragma=synchronous(FULL)")
if err != nil {
return fmt.Errorf("failed to open bans db: %w", err)
}
defer func() {
err = db_b.Close()
if err != nil {
fmt.Println(err)
}
}()
_, err = db_b.Exec(CreateBansTable)
if err != nil {
return fmt.Errorf("failed to create bans table: %w", err)
}
fmt.Println("Tables created successfully!")
return nil
return errors.Join(err1, err2)
}

View File

@@ -7,15 +7,15 @@ import (
_ "modernc.org/sqlite"
)
type Request_Writer struct {
type RequestWriter struct {
logger *logger.Logger
db *sql.DB
}
func NewRequestsWr() (*Request_Writer, error) {
func NewRequestsWr() (*RequestWriter, error) {
db, err := sql.Open(
"sqlite",
"/var/lib/banforge/requests.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(30000)&_pragma=synchronous(NORMAL)",
buildSqliteDsn(ReqDBPath, pragmas),
)
if err != nil {
return nil, err
@@ -23,7 +23,7 @@ func NewRequestsWr() (*Request_Writer, error) {
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
db.SetConnMaxLifetime(0)
return &Request_Writer{
return &RequestWriter{
logger: logger.New(false),
db: db,
}, nil

View File

@@ -1,10 +1,13 @@
package storage
import (
"database/sql"
"errors"
"fmt"
"time"
)
func WriteReq(db *Request_Writer, resultCh <-chan *LogEntry) {
func WriteReq(db *RequestWriter, resultCh <-chan *LogEntry) {
db.logger.Info("Starting log writer")
const batchSize = 100
const flushInterval = 1 * time.Second
@@ -14,29 +17,36 @@ func WriteReq(db *Request_Writer, resultCh <-chan *LogEntry) {
defer ticker.Stop()
flush := func() {
defer db.logger.Debug("Flushed batch", "count", len(batch))
err := func() (err error) {
if len(batch) == 0 {
return
return nil
}
tx, err := db.db.Begin()
if err != nil {
db.logger.Error("Failed to begin transaction", "error", err)
return
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer func() {
if rollbackErr := tx.Rollback(); rollbackErr != nil &&
!errors.Is(rollbackErr, sql.ErrTxDone) {
err = errors.Join(
err,
fmt.Errorf("failed to rollback transaction: %w", rollbackErr),
)
}
}()
stmt, err := tx.Prepare(
"INSERT INTO requests (service, ip, path, method, status, created_at) VALUES (?, ?, ?, ?, ?, ?)",
)
if err != nil {
db.logger.Error("Failed to prepare statement", "error", err)
if rollbackErr := tx.Rollback(); rollbackErr != nil {
db.logger.Error("Failed to rollback transaction", "error", rollbackErr)
}
return
err = fmt.Errorf("failed to prepare statement: %w", err)
return err
}
defer func() {
if closeErr := stmt.Close(); closeErr != nil {
db.logger.Error("Failed to close statement", "error", closeErr)
err = errors.Join(err, fmt.Errorf("failed to close statement: %w", closeErr))
}
}()
@@ -50,17 +60,20 @@ func WriteReq(db *Request_Writer, resultCh <-chan *LogEntry) {
time.Now().Format(time.RFC3339),
)
if err != nil {
db.logger.Error("Failed to insert entry", "error", err)
db.logger.Error(fmt.Errorf("failed to insert entry: %w", err).Error())
}
}
if err := tx.Commit(); err != nil {
db.logger.Error("Failed to commit transaction", "error", err)
return
return fmt.Errorf("failed to commit transaction: %w", err)
}
db.logger.Debug("Flushed batch", "count", len(batch))
batch = batch[:0]
return err
}()
if err != nil {
db.logger.Error(err.Error())
}
}
for {

View File

@@ -277,7 +277,7 @@ func TestWrite_ChannelClosed(t *testing.T) {
}
}
func NewRequestWriterWithDBPath(dbPath string) (*Request_Writer, error) {
func NewRequestWriterWithDBPath(dbPath string) (*RequestWriter, error) {
db, err := sql.Open("sqlite", dbPath+"?_pragma=journal_mode(WAL)&_pragma=busy_timeout(30000)&_pragma=synchronous(NORMAL)")
if err != nil {
return nil, err
@@ -285,13 +285,13 @@ func NewRequestWriterWithDBPath(dbPath string) (*Request_Writer, error) {
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
db.SetConnMaxLifetime(0)
return &Request_Writer{
return &RequestWriter{
logger: logger.New(false),
db: db,
}, nil
}
func (w *Request_Writer) CreateTable() error {
func (w *RequestWriter) CreateTable() error {
_, err := w.db.Exec(CreateRequestsTable)
if err != nil {
return err
@@ -300,7 +300,7 @@ func (w *Request_Writer) CreateTable() error {
return nil
}
func (w *Request_Writer) Close() error {
func (w *RequestWriter) Close() error {
w.logger.Info("Closing request database connection")
err := w.db.Close()
if err != nil {
@@ -309,7 +309,7 @@ func (w *Request_Writer) Close() error {
return nil
}
func (w *Request_Writer) GetRequestCount() (int, error) {
func (w *RequestWriter) GetRequestCount() (int, error) {
var count int
err := w.db.QueryRow("SELECT COUNT(*) FROM requests").Scan(&count)
if err != nil {