9 Commits

Author SHA1 Message Date
d3m0k1d
b9754f605b fix: Delete sudo calls on exec
All checks were successful
build / build (push) Successful in 3m8s
CD - BanForge Release / release (push) Successful in 5m24s
2026-01-27 16:20:03 +03:00
d3m0k1d
be6b19426b docs: Add installation guide
All checks were successful
build / build (push) Successful in 3m16s
2026-01-26 16:51:40 +03:00
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
13 changed files with 139 additions and 56 deletions

View File

@@ -15,14 +15,15 @@ Log-based IPS system written in Go for Linux-based system.
# Overview
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) because Github has limits for Actions.
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.
If you have any questions or suggestions, create issue on [Github](https://github.com/d3m0k1d/BanForge/issues).
## Roadmap
- [x] Real-time Nginx log monitoring
- [ ] Add support for other service
- [ ] Add support for user service with regular expressions
- [x] Rule system
- [x] Nginx and Sshd support
- [x] Working with ufw/iptables/nftables/firewalld
- [ ] Add support for most popular web-service
- [ ] User regexp for custom services
- [ ] TUI interface
# Requirements
@@ -31,15 +32,79 @@ If you have any questions or suggestions, create issue on [Github](https://githu
- ufw/iptables/nftables/firewalld
# 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.
Or clone the repo and use the Makefile.
```
git clone https://gitea.d3m0k1d.ru/d3m0k1d/BanForge.git
cd BanForge
sudo make build-daemon
cd bin
Search for a release on the [Gitea](https://gitea.d3m0k1d.ru/d3m0k1d/BanForge/releases) releases page and download it.
In release page you can find rpm, deb, apk packages, for amd or arm architecture.
## Installation guide for packages
### Debian/Ubuntu(.deb)
```bash
# Download the latest DEB package
wget https://gitea.d3m0k1d.ru/d3m0k1d/BanForge/releases/download/v0.4.0/banforge_0.4.0_linux_amd64.deb
# Install
sudo dpkg -i banforge_0.4.0_linux_amd64.deb
# Verify installation
sudo systemctl status banforge
```
### RHEL-based(.rpm)
```bash
# Download
wget https://gitea.d3m0k1d.ru/d3m0k1d/BanForge/releases/download/v0.4.0/banforge_0.4.0_linux_amd64.rpm
# Install
sudo rpm -i banforge_0.4.0_linux_amd64.rpm
# Or with dnf (CentOS 8+, AlmaLinux)
sudo dnf install banforge_0.4.0_linux_amd64.rpm
# Verify
sudo systemctl status banforge
```
### Alpine(.apk)
```bash
# Download
wget https://gitea.d3m0k1d.ru/d3m0k1d/BanForge/releases/download/v0.4.0/banforge_0.4.0_linux_amd64.apk
# Install
sudo apk add --allow-untrusted banforge_0.4.0_linux_amd64.apk
# Verify
sudo rc-service banforge status
```
### Arch Linux(.pkg.tar.zst)
```bash
# Download
wget https://gitea.d3m0k1d.ru/d3m0k1d/BanForge/releases/download/v0.4.0/banforge_0.4.0_linux_amd64.pkg.tar.zst
# Install
sudo pacman -U banforge_0.4.0_linux_amd64.pkg.tar.zst
# Verify
sudo systemctl status banforge
```
This is examples for other versions with different architecture or new versions check release page on [Gitea](https://gitea.d3m0k1d.ru/d3m0k1d/BanForge/releases).
## Installation guide for source code
```bash
# Download
git clone https://github.com/d3m0k1d/BanForge.git
cd BanForge
make build-daemon
cd bin
mv banforge /usr/bin/banforge
cd ..
# Add init script and uses banforge init
cd build
./postinstall.sh
```
# Usage
For first steps use this commands
```bash

View File

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

View File

@@ -12,12 +12,20 @@ import (
)
var (
ip string
ttl_fw string
)
var UnbanCmd = &cobra.Command{
Use: "unban",
Short: "Unban IP",
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()
if err != nil {
fmt.Println(err)
@@ -60,6 +68,14 @@ var BanCmd = &cobra.Command{
Use: "ban",
Short: "Ban IP",
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()
if err != nil {
fmt.Println(err)
@@ -89,7 +105,7 @@ var BanCmd = &cobra.Command{
fmt.Println(err)
os.Exit(1)
}
err = db.AddBan(ip, "1y")
err = db.AddBan(ip, ttl_fw, "manual ban")
if err != nil {
fmt.Println(err)
os.Exit(1)
@@ -99,6 +115,5 @@ var BanCmd = &cobra.Command{
}
func FwRegister() {
BanCmd.Flags().StringVarP(&ip, "ip", "i", "", "ip to ban")
UnbanCmd.Flags().StringVarP(&ip, "ip", "i", "", "ip to unban")
BanCmd.Flags().StringVarP(&ttl_fw, "ttl", "t", "", "ban time")
}

View File

@@ -31,6 +31,7 @@ banforge unban <ip>
**Description**
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
```shell
banforge list

View File

@@ -21,14 +21,14 @@ func (f *Firewalld) Ban(ip string) error {
if err != nil {
return err
}
cmd := exec.Command("sudo", "firewall-cmd", "--zone=drop", "--add-source", ip, "--permanent")
cmd := exec.Command("firewall-cmd", "--zone=drop", "--add-source", ip, "--permanent")
output, err := cmd.CombinedOutput()
if err != nil {
f.logger.Error(err.Error())
return err
}
f.logger.Info("Add source " + ip + " " + string(output))
output, err = exec.Command("sudo", "firewall-cmd", "--reload").CombinedOutput()
output, err = exec.Command("firewall-cmd", "--reload").CombinedOutput()
if err != nil {
f.logger.Error(err.Error())
return err
@@ -42,14 +42,14 @@ func (f *Firewalld) Unban(ip string) error {
if err != nil {
return err
}
cmd := exec.Command("sudo", "firewall-cmd", "--zone=drop", "--remove-source", ip, "--permanent")
cmd := exec.Command("firewall-cmd", "--zone=drop", "--remove-source", ip, "--permanent")
output, err := cmd.CombinedOutput()
if err != nil {
f.logger.Error(err.Error())
return err
}
f.logger.Info("Remove source " + ip + " " + string(output))
output, err = exec.Command("sudo", "firewall-cmd", "--reload").CombinedOutput()
output, err = exec.Command("firewall-cmd", "--reload").CombinedOutput()
if err != nil {
f.logger.Error(err.Error())
return err

View File

@@ -27,7 +27,7 @@ func (f *Iptables) Ban(ip string) error {
if err != nil {
return err
}
cmd := exec.Command("sudo", "iptables", "-A", "INPUT", "-s", ip, "-j", "DROP")
cmd := exec.Command("iptables", "-A", "INPUT", "-s", ip, "-j", "DROP")
output, err := cmd.CombinedOutput()
if err != nil {
f.logger.Error("failed to ban IP",
@@ -45,7 +45,7 @@ func (f *Iptables) Ban(ip string) error {
return err
}
// #nosec G204 - f.config is validated above via validateConfigPath()
cmd = exec.Command("sudo", "iptables-save", "-f", f.config)
cmd = exec.Command("iptables-save", "-f", f.config)
output, err = cmd.CombinedOutput()
if err != nil {
f.logger.Error("failed to save config",
@@ -69,7 +69,7 @@ func (f *Iptables) Unban(ip string) error {
if err != nil {
return err
}
cmd := exec.Command("sudo", "iptables", "-D", "INPUT", "-s", ip, "-j", "DROP")
cmd := exec.Command("iptables", "-D", "INPUT", "-s", ip, "-j", "DROP")
output, err := cmd.CombinedOutput()
if err != nil {
f.logger.Error("failed to unban IP",
@@ -87,7 +87,7 @@ func (f *Iptables) Unban(ip string) error {
return err
}
// #nosec G204 - f.config is validated above via validateConfigPath()
cmd = exec.Command("sudo", "iptables-save", "-f", f.config)
cmd = exec.Command("iptables-save", "-f", f.config)
output, err = cmd.CombinedOutput()
if err != nil {
f.logger.Error("failed to save config",

View File

@@ -26,7 +26,7 @@ func (n *Nftables) Ban(ip string) error {
return err
}
cmd := exec.Command("sudo", "nft", "add", "rule", "inet", "banforge", "banned",
cmd := exec.Command("nft", "add", "rule", "inet", "banforge", "banned",
"ip", "saddr", ip, "drop")
output, err := cmd.CombinedOutput()
if err != nil {
@@ -70,7 +70,7 @@ func (n *Nftables) Unban(ip string) error {
return fmt.Errorf("no rule found for IP %s", ip)
}
// #nosec G204 - handle is extracted from nftables output and validated
cmd := exec.Command("sudo", "nft", "delete", "rule", "inet", "banforge", "banned",
cmd := exec.Command("nft", "delete", "rule", "inet", "banforge", "banned",
"handle", handle)
output, err := cmd.CombinedOutput()
if err != nil {
@@ -112,7 +112,7 @@ func (n *Nftables) Setup(config string) error {
}
}
`
cmd := exec.Command("sudo", "tee", config)
cmd := exec.Command("tee", config)
stdin, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("failed to create stdin pipe: %w", err)
@@ -135,7 +135,7 @@ func (n *Nftables) Setup(config string) error {
return fmt.Errorf("failed to save config: %w", err)
}
cmd = exec.Command("sudo", "nft", "-f", config)
cmd = exec.Command("nft", "-f", config)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to load nftables config: %s", string(output))
@@ -145,7 +145,7 @@ func (n *Nftables) Setup(config string) error {
}
func (n *Nftables) findRuleHandle(ip string) (string, error) {
cmd := exec.Command("sudo", "nft", "-a", "list", "chain", "inet", "banforge", "banned")
cmd := exec.Command("nft", "-a", "list", "chain", "inet", "banforge", "banned")
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("failed to list chain rules: %w", err)
@@ -172,13 +172,13 @@ func saveNftablesConfig(configPath string) error {
return err
}
cmd := exec.Command("sudo", "nft", "list", "ruleset")
cmd := exec.Command("nft", "list", "ruleset")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to get nftables ruleset: %w", err)
}
cmd = exec.Command("sudo", "tee", configPath)
cmd = exec.Command("tee", configPath)
stdin, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("failed to create stdin pipe: %w", err)

View File

@@ -23,7 +23,7 @@ func (u *Ufw) Ban(ip string) error {
return err
}
cmd := exec.Command("sudo", "ufw", "--force", "deny", "from", ip)
cmd := exec.Command("ufw", "--force", "deny", "from", ip)
output, err := cmd.CombinedOutput()
if err != nil {
u.logger.Error("failed to ban IP",
@@ -42,7 +42,7 @@ func (u *Ufw) Unban(ip string) error {
return err
}
cmd := exec.Command("sudo", "ufw", "--force", "delete", "deny", "from", ip)
cmd := exec.Command("ufw", "--force", "delete", "deny", "from", ip)
output, err := cmd.CombinedOutput()
if err != nil {
u.logger.Error("failed to unban IP",
@@ -59,7 +59,7 @@ func (u *Ufw) Unban(ip string) error {
func (u *Ufw) Setup(config string) error {
if config != "" {
fmt.Printf("Ufw dont support config file\n")
cmd := exec.Command("sudo", "ufw", "enable")
cmd := exec.Command("ufw", "enable")
output, err := cmd.CombinedOutput()
if err != nil {
u.logger.Error("failed to enable ufw",
@@ -69,7 +69,7 @@ func (u *Ufw) Setup(config string) error {
}
}
if config == "" {
cmd := exec.Command("sudo", "ufw", "enable")
cmd := exec.Command("ufw", "enable")
output, err := cmd.CombinedOutput()
if err != nil {
u.logger.Error("failed to enable ufw",

View File

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

View File

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

View File

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

View File

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

View File

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