7 Commits

Author SHA1 Message Date
d3m0k1d
3ebffda2c7 feat: improve table on cli interface
All checks were successful
build / build (push) Successful in 3m14s
CD - BanForge Release / release (push) Successful in 5m8s
2026-01-26 14:21:35 +03:00
d3m0k1d
cadbbc9080 feat: improve reason string on db
All checks were successful
build / build (push) Successful in 3m8s
2026-01-26 14:04:30 +03:00
d3m0k1d
e907fb0b1a feat: update ban/unban command
All checks were successful
build / build (push) Successful in 3m13s
2026-01-25 21:13:56 +03:00
d3m0k1d
b0fc0646d2 fix: typo
All checks were successful
build / build (push) Successful in 3m23s
2026-01-24 20:30:27 +03:00
d3m0k1d
c2eb02afc7 docs: fix roadmap
All checks were successful
build / build (push) Successful in 3m22s
2026-01-23 18:22:18 +03:00
d3m0k1d
262f3daee4 docs: update reaadme.md Roadmap and overview
All checks were successful
build / build (push) Successful in 3m3s
2026-01-23 17:58:03 +03:00
d3m0k1d
fb32886d4a refactoring: rename func writer
All checks were successful
build / build (push) Successful in 3m31s
2026-01-22 21:08:55 +03:00
9 changed files with 50 additions and 31 deletions

View File

@@ -15,14 +15,15 @@ Log-based IPS system written in Go for Linux-based system.
# Overview # Overview
BanForge is a simple IPS for replacement fail2ban in Linux system. BanForge is a simple IPS for replacement fail2ban in Linux system.
The project is currently in its early stages of development. All release are available on my self-hosted [Gitea](https://gitea.d3m0k1d.ru/d3m0k1d/BanForge) after release v1.0.0 are available on Github release page.
All release are available on my self-hosted [Gitea](https://gitea.d3m0k1d.ru/d3m0k1d/BanForge) because Github has limits for Actions.
If you have any questions or suggestions, create issue on [Github](https://github.com/d3m0k1d/BanForge/issues). If you have any questions or suggestions, create issue on [Github](https://github.com/d3m0k1d/BanForge/issues).
## Roadmap ## Roadmap
- [x] Real-time Nginx log monitoring - [x] Rule system
- [ ] Add support for other service - [x] Nginx and Sshd support
- [ ] Add support for user service with regular expressions - [x] Working with ufw/iptables/nftables/firewalld
- [ ] Add support for most popular web-service
- [ ] User regexp for custom services
- [ ] TUI interface - [ ] TUI interface
# Requirements # Requirements
@@ -31,7 +32,7 @@ If you have any questions or suggestions, create issue on [Github](https://githu
- ufw/iptables/nftables/firewalld - ufw/iptables/nftables/firewalld
# Installation # Installation
Search for a release on the [Gitea](https://gitea.d3m0k1d.ru/d3m0k1d/BanForge/releases) releases page and download it. Then create or copy a systemd unit file. Search for a release on the [Gitea](https://gitea.d3m0k1d.ru/d3m0k1d/BanForge/releases) releases page and download it. Then create or copy(/build dir) a systemd unit(openrc script) file.
Or clone the repo and use the Makefile. Or clone the repo and use the Makefile.
``` ```
git clone https://gitea.d3m0k1d.ru/d3m0k1d/BanForge.git git clone https://gitea.d3m0k1d.ru/d3m0k1d/BanForge.git

View File

@@ -67,7 +67,7 @@ var DaemonCmd = &cobra.Command{
j.LoadRules(r) j.LoadRules(r)
go j.UnbanChecker() go j.UnbanChecker()
go j.Tribunal() go j.Tribunal()
go storage.Write(reqDb_w, resultCh) go storage.WriteReq(reqDb_w, resultCh)
var scanners []*parser.Scanner var scanners []*parser.Scanner
for _, svc := range cfg.Service { for _, svc := range cfg.Service {

View File

@@ -12,12 +12,20 @@ import (
) )
var ( var (
ip string ttl_fw string
) )
var UnbanCmd = &cobra.Command{ var UnbanCmd = &cobra.Command{
Use: "unban", Use: "unban",
Short: "Unban IP", Short: "Unban IP",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
fmt.Println("IP can't be empty")
os.Exit(1)
}
if ttl_fw == "" {
ttl_fw = "1y"
}
ip := args[0]
db, err := storage.NewBanWriter() db, err := storage.NewBanWriter()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@@ -60,6 +68,14 @@ var BanCmd = &cobra.Command{
Use: "ban", Use: "ban",
Short: "Ban IP", Short: "Ban IP",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
fmt.Println("IP can't be empty")
os.Exit(1)
}
if ttl_fw == "" {
ttl_fw = "1y"
}
ip := args[0]
db, err := storage.NewBanWriter() db, err := storage.NewBanWriter()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@@ -89,7 +105,7 @@ var BanCmd = &cobra.Command{
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
err = db.AddBan(ip, "1y") err = db.AddBan(ip, ttl_fw, "manual ban")
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
@@ -99,6 +115,5 @@ var BanCmd = &cobra.Command{
} }
func FwRegister() { func FwRegister() {
BanCmd.Flags().StringVarP(&ip, "ip", "i", "", "ip to ban") BanCmd.Flags().StringVarP(&ttl_fw, "ttl", "t", "", "ban time")
UnbanCmd.Flags().StringVarP(&ip, "ip", "i", "", "ip to unban")
} }

View File

@@ -31,6 +31,7 @@ banforge unban <ip>
**Description** **Description**
These commands provide an abstraction over your firewall. If you want to simplify the interface to your firewall, you can use these commands. These commands provide an abstraction over your firewall. If you want to simplify the interface to your firewall, you can use these commands.
Flag -t or -ttl add bantime if not used default ban 1 year
### list - Lists the IP addresses that are currently blocked ### list - Lists the IP addresses that are currently blocked
```shell ```shell
banforge list banforge list

View File

@@ -100,7 +100,7 @@ func (j *Judge) Tribunal() {
break break
} }
err = j.db_w.AddBan(entry.IP, rule.BanTime) err = j.db_w.AddBan(entry.IP, rule.BanTime, rule.Name)
if err != nil { if err != nil {
j.logger.Error( j.logger.Error(
"Failed to add ban to database", "Failed to add ban to database",

View File

@@ -41,7 +41,7 @@ func (d *BanWriter) CreateTable() error {
return nil return nil
} }
func (d *BanWriter) AddBan(ip string, ttl string) error { func (d *BanWriter) AddBan(ip string, ttl string, reason string) error {
duration, err := config.ParseDurationWithYears(ttl) duration, err := config.ParseDurationWithYears(ttl)
if err != nil { if err != nil {
d.logger.Error("Invalid duration format", "ttl", ttl, "error", err) d.logger.Error("Invalid duration format", "ttl", ttl, "error", err)
@@ -54,7 +54,7 @@ func (d *BanWriter) AddBan(ip string, ttl string) error {
_, err = d.db.Exec( _, err = d.db.Exec(
"INSERT INTO bans (ip, reason, banned_at, expired_at) VALUES (?, ?, ?, ?)", "INSERT INTO bans (ip, reason, banned_at, expired_at) VALUES (?, ?, ?, ?)",
ip, ip,
"1", reason,
now.Format(time.RFC3339), now.Format(time.RFC3339),
expiredAt.Format(time.RFC3339), expiredAt.Format(time.RFC3339),
) )
@@ -180,8 +180,8 @@ func (d *BanReader) BanList() error {
t := table.NewWriter() t := table.NewWriter()
t.SetOutputMirror(os.Stdout) t.SetOutputMirror(os.Stdout)
t.SetStyle(table.StyleBold) t.SetStyle(table.StyleBold)
t.AppendHeader(table.Row{"№", "IP", "Banned At"}) t.AppendHeader(table.Row{"№", "IP", "Banned At", "Reason", "Expires At"})
rows, err := d.db.Query("SELECT ip, banned_at FROM bans") rows, err := d.db.Query("SELECT ip, banned_at, reason, expired_at FROM bans")
if err != nil { if err != nil {
d.logger.Error("Failed to get ban list", "error", err) d.logger.Error("Failed to get ban list", "error", err)
return err return err
@@ -190,12 +190,14 @@ func (d *BanReader) BanList() error {
count++ count++
var ip string var ip string
var bannedAt string var bannedAt string
err := rows.Scan(&ip, &bannedAt) var reason string
var expiredAt string
err := rows.Scan(&ip, &bannedAt, &reason, &expiredAt)
if err != nil { if err != nil {
d.logger.Error("Failed to get ban list", "error", err) d.logger.Error("Failed to get ban list", "error", err)
return err return err
} }
t.AppendRow(table.Row{count, ip, bannedAt}) t.AppendRow(table.Row{count, ip, bannedAt, reason, expiredAt})
} }
t.Render() t.Render()

View File

@@ -26,7 +26,7 @@ func TestBanWriter_AddBan(t *testing.T) {
ip := "192.168.1.1" ip := "192.168.1.1"
ttl := "1h" ttl := "1h"
err = writer.AddBan(ip, ttl) err = writer.AddBan(ip, ttl, "test")
if err != nil { if err != nil {
t.Errorf("AddBan failed: %v", err) t.Errorf("AddBan failed: %v", err)
} }
@@ -62,7 +62,7 @@ func TestBanWriter_RemoveBan(t *testing.T) {
} }
ip := "192.168.1.2" ip := "192.168.1.2"
err = writer.AddBan(ip, "1h") err = writer.AddBan(ip, "1h", "test")
if err != nil { if err != nil {
t.Fatalf("Failed to add ban: %v", err) t.Fatalf("Failed to add ban: %v", err)
} }
@@ -111,13 +111,13 @@ func TestBanWriter_RemoveExpiredBans(t *testing.T) {
} }
expiredIP := "192.168.1.3" expiredIP := "192.168.1.3"
err = writer.AddBan(expiredIP, "-1h") err = writer.AddBan(expiredIP, "-1h", "tes")
if err != nil { if err != nil {
t.Fatalf("Failed to add expired ban: %v", err) t.Fatalf("Failed to add expired ban: %v", err)
} }
activeIP := "192.168.1.4" activeIP := "192.168.1.4"
err = writer.AddBan(activeIP, "1h") err = writer.AddBan(activeIP, "1h", "test")
if err != nil { if err != nil {
t.Fatalf("Failed to add active ban: %v", err) t.Fatalf("Failed to add active ban: %v", err)
} }
@@ -181,7 +181,7 @@ func TestBanReader_IsBanned(t *testing.T) {
} }
ip := "192.168.1.5" ip := "192.168.1.5"
err = writer.AddBan(ip, "1h") err = writer.AddBan(ip, "1h", "test")
if err != nil { if err != nil {
t.Fatalf("Failed to add ban: %v", err) t.Fatalf("Failed to add ban: %v", err)
} }
@@ -280,7 +280,7 @@ func TestBanWriter_AddBan_InvalidDuration(t *testing.T) {
t.Fatalf("Failed to create table: %v", err) t.Fatalf("Failed to create table: %v", err)
} }
err = writer.AddBan("192.168.1.7", "invalid_duration") err = writer.AddBan("192.168.1.7", "invalid_duration", "test")
if err == nil { if err == nil {
t.Error("Expected error for invalid duration") t.Error("Expected error for invalid duration")
} else if err.Error() == "" || err.Error() == "<nil>" { } else if err.Error() == "" || err.Error() == "<nil>" {
@@ -306,7 +306,7 @@ func TestMultipleBans(t *testing.T) {
ips := []string{"192.168.1.8", "192.168.1.9", "192.168.1.10"} ips := []string{"192.168.1.8", "192.168.1.9", "192.168.1.10"}
for _, ip := range ips { for _, ip := range ips {
err := writer.AddBan(ip, "1h") err := writer.AddBan(ip, "1h", "test")
if err != nil { if err != nil {
t.Errorf("Failed to add ban for IP %s: %v", ip, err) t.Errorf("Failed to add ban for IP %s: %v", ip, err)
} }

View File

@@ -4,7 +4,7 @@ import (
"time" "time"
) )
func Write(db *Request_Writer, resultCh <-chan *LogEntry) { func WriteReq(db *Request_Writer, resultCh <-chan *LogEntry) {
db.logger.Info("Starting log writer") db.logger.Info("Starting log writer")
const batchSize = 100 const batchSize = 100
const flushInterval = 1 * time.Second const flushInterval = 1 * time.Second

View File

@@ -28,7 +28,7 @@ func TestWrite_BatchInsert(t *testing.T) {
done := make(chan bool) done := make(chan bool)
go func() { go func() {
Write(writer, resultCh) WriteReq(writer, resultCh)
close(done) close(done)
}() }()
@@ -115,7 +115,7 @@ func TestWrite_BatchSizeTrigger(t *testing.T) {
resultCh := make(chan *LogEntry, 100) resultCh := make(chan *LogEntry, 100)
done := make(chan bool) done := make(chan bool)
go func() { go func() {
Write(writer, resultCh) WriteReq(writer, resultCh)
close(done) close(done)
}() }()
@@ -167,7 +167,7 @@ func TestWrite_FlushInterval(t *testing.T) {
done := make(chan bool) done := make(chan bool)
go func() { go func() {
Write(writer, resultCh) WriteReq(writer, resultCh)
close(done) close(done)
}() }()
@@ -216,7 +216,7 @@ func TestWrite_EmptyBatch(t *testing.T) {
done := make(chan bool) done := make(chan bool)
go func() { go func() {
Write(writer, resultCh) WriteReq(writer, resultCh)
close(done) close(done)
}() }()
@@ -250,7 +250,7 @@ func TestWrite_ChannelClosed(t *testing.T) {
done := make(chan bool) done := make(chan bool)
go func() { go func() {
Write(writer, resultCh) WriteReq(writer, resultCh)
close(done) close(done)
}() }()